diff --git a/classes/submission/genre/Genre.php b/classes/submission/genre/Genre.php new file mode 100644 index 00000000000..7fcc0c349ad --- /dev/null +++ b/classes/submission/genre/Genre.php @@ -0,0 +1,93 @@ +hasMany(SubmissionFile::class, 'genre_id'); + } + + + // Accessors and Mutators + protected function entryKey(): Attribute + { + return Attribute::make( + get: fn($value) => strtoupper($value), + set: fn($value) => strtolower($value) + ); + } + + protected function enabled(): Attribute + { + return Attribute::make( + get: fn($value) => (bool) $value, + set: fn($value) => (int) $value + ); + } + + // Scopes + public function scopeEnabled(Builder $query): Builder + { + return $query->where('enabled', 1); + } + + public function scopeDependent(Builder $query, bool $dependent = true): Builder + { + return $query->where('dependent', $dependent); + } + + public function scopeSupplementary(Builder $query, bool $supplementary = true): Builder + { + return $query->where('supplementary', $supplementary); + } + + public function scopeRequired(Builder $query, bool $required = true): Builder + { + return $query->where('required', $required); + } + + public function scopeInContext(Builder $query, int $contextId): Builder + { + return $query->where('context_id', $contextId); + } + + // Business Logic + public function isDefault(): bool + { + $defaultKeys = $this->getDefaultKeys(); + return in_array($this->entry_key, $defaultKeys); + } + + /** + * Retrieve a list of default genre keys from the database or configuration. + */ + protected function getDefaultKeys(): array + { + // fetching default keys from a configuration or database + return DB::table('genre_defaults')->pluck('entry_key')->toArray(); + } +} diff --git a/classes/submission/genre/GenreDAO.php b/classes/submission/genre/GenreDAO.php new file mode 100644 index 00000000000..e8c48f2f2e0 --- /dev/null +++ b/classes/submission/genre/GenreDAO.php @@ -0,0 +1,464 @@ +retrieve( + 'SELECT * FROM genres WHERE genre_id = ?' . + ($contextId ? ' AND context_id = ?' : '') . + ' ORDER BY seq', + $params + ); + $row = $result->current(); + return $row ? $this->_fromRow((array) $row) : null; + } + + /** + * Retrieve all genres + * + * @param int $contextId + * @param ?\PKP\db\DBResultRange $rangeInfo optional + * + * @return DAOResultFactory containing matching genres + */ + public function getEnabledByContextId($contextId, $rangeInfo = null) + { + $result = $this->retrieveRange( + 'SELECT * FROM genres + WHERE enabled = ? AND context_id = ? + ORDER BY seq', + [1, (int) $contextId], + $rangeInfo + ); + + return new DAOResultFactory($result, $this, '_fromRow', ['id']); + } + + /** + * Retrieve genres based on whether they are dependent or not. + * + * @param bool $dependentFilesOnly + * @param int $contextId + * @param ?\PKP\db\DBResultRange $rangeInfo optional + * + * @return DAOResultFactory containing matching genres + */ + public function getByDependenceAndContextId($dependentFilesOnly, $contextId, $rangeInfo = null) + { + $result = $this->retrieveRange( + 'SELECT * FROM genres + WHERE enabled = ? AND context_id = ? AND dependent = ? + ORDER BY seq', + [1, (int) $contextId, (int) $dependentFilesOnly], + $rangeInfo + ); + + return new DAOResultFactory($result, $this, '_fromRow', ['id']); + } + + /** + * Retrieve genres based on whether they are supplementary or not. + * + * @param bool $supplementaryFilesOnly + * @param int $contextId + * @param ?\PKP\db\DBResultRange $rangeInfo optional + * + * @return DAOResultFactory + */ + public function getBySupplementaryAndContextId($supplementaryFilesOnly, $contextId, $rangeInfo = null) + { + $result = $this->retrieveRange( + 'SELECT * FROM genres + WHERE enabled = ? AND context_id = ? AND supplementary = ? + ORDER BY seq', + [1, (int) $contextId, (int) $supplementaryFilesOnly], + $rangeInfo + ); + + return new DAOResultFactory($result, $this, '_fromRow', ['id']); + } + + /** + * Retrieve genres that are not supplementary or dependent. + * + * @param int $contextId + * @param ?\PKP\db\DBResultRange $rangeInfo optional + * + * @return DAOResultFactory + */ + public function getPrimaryByContextId($contextId, $rangeInfo = null) + { + $result = $this->retrieveRange( + 'SELECT * FROM genres + WHERE enabled = ? AND context_id = ? AND dependent = ? AND supplementary = ? + ORDER BY seq', + [1, (int) $contextId, 0, 0], + $rangeInfo + ); + + return new DAOResultFactory($result, $this, '_fromRow', ['id']); + } + + /** + * Retrieve all genres + * + * @param int $contextId + * @param ?\PKP\db\DBResultRange $rangeInfo optional + * + * @return DAOResultFactory containing matching genres + */ + public function getByContextId($contextId, $rangeInfo = null) + { + $result = $this->retrieveRange( + 'SELECT * FROM genres WHERE context_id = ? ORDER BY seq', + [(int) $contextId], + $rangeInfo + ); + + return new DAOResultFactory($result, $this, '_fromRow', ['id']); + } + + /** + * Get genres that are required for a new + * submission in a context + */ + public function getRequiredToSubmit(int $contextId): Collection + { + return DB::table('genres') + ->where('context_id', $contextId) + ->where('required', 1) + ->get() + ->map(function (object $row) { + return $this->_fromRow((array) $row); + }); + } + + /** + * Retrieves the genre associated with a key. + * + * @param string $key the entry key + * @param int $contextId Optional context ID + * + * @return Genre + */ + public function getByKey($key, $contextId = null) + { + $params = [$key]; + if ($contextId) { + $params[] = (int) $contextId; + } + + $result = $this->retrieve( + 'SELECT * FROM genres WHERE entry_key = ? ' . + ($contextId ? ' AND context_id = ?' : ''), + $params + ); + + $row = $result->current(); + return $row ? $this->_fromRow((array) $row) : null; + } + + /** + * Get a list of field names for which data is localized. + * + * @return array + */ + public function getLocaleFieldNames() + { + return ['name']; + } + + /** + * Update the settings for this object + * + * @param object $genre + */ + public function updateLocaleFields($genre) + { + $this->updateDataObjectSettings( + 'genre_settings', + $genre, + ['genre_id' => $genre->getId()] + ); + } + + /** + * Construct a new data object corresponding to this DAO. + * + * @return Genre + */ + public function newDataObject() + { + return new Genre(); + } + + /** + * Internal function to return a Genre object from a row. + * + * @param array $row + * + * @return Genre + * + * @hook GenreDAO::_fromRow [[&$genre, &$row]] + */ + public function _fromRow($row) + { + $genre = $this->newDataObject(); + $genre->setId((int) $row['genre_id']); + $genre->setKey($row['entry_key']); + $genre->setContextId($row['context_id']); + $genre->setCategory((int) $row['category']); + $genre->setDependent($row['dependent']); + $genre->setSupplementary($row['supplementary']); + $genre->setRequired($row['required']); + $genre->setSequence($row['seq']); + $genre->setEnabled($row['enabled']); + + $this->getDataObjectSettings('genre_settings', 'genre_id', $row['genre_id'], $genre); + + Hook::call('GenreDAO::_fromRow', [&$genre, &$row]); + + return $genre; + } + + /** + * Insert a new genre. + * + * @param Genre $genre + * + * @return int Inserted genre ID + */ + public function insertObject($genre) + { + $this->update( + 'INSERT INTO genres + (entry_key, seq, context_id, category, dependent, supplementary, required) + VALUES + (?, ?, ?, ?, ?, ?, ?)', + [ + $genre->getKey(), + (float) $genre->getSequence(), + (int) $genre->getContextId(), + (int) $genre->getCategory(), + $genre->getDependent() ? 1 : 0, + $genre->getSupplementary() ? 1 : 0, + $genre->getRequired() ? 1 : 0, + ] + ); + + $genre->setId($this->getInsertId()); + $this->updateLocaleFields($genre); + return $genre->getId(); + } + + /** + * Update an existing genre. + * + * @param Genre $genre + */ + public function updateObject($genre) + { + $this->update( + 'UPDATE genres + SET entry_key = ?, + seq = ?, + dependent = ?, + supplementary = ?, + enabled = ?, + category = ?, + required = ? + WHERE genre_id = ?', + [ + $genre->getKey(), + (float) $genre->getSequence(), + $genre->getDependent() ? 1 : 0, + $genre->getSupplementary() ? 1 : 0, + $genre->getEnabled() ? 1 : 0, + $genre->getCategory(), + $genre->getRequired() ? 1 : 0, + (int) $genre->getId(), + ] + ); + $this->updateLocaleFields($genre); + } + + /** + * Delete a genre by id. + * + * @param Genre $genre + */ + public function deleteObject($genre) + { + return $this->deleteById($genre->getId()); + } + + /** + * Soft delete a genre by id. + * + * @param int $genreId Genre ID + */ + public function deleteById($genreId) + { + return $this->update( + 'UPDATE genres SET enabled = ? WHERE genre_id = ?', + [0, (int) $genreId] + ); + } + + /** + * Delete the genre entries associated with a context. + * Called when deleting a Context in ContextDAO. + * + * @param int $contextId Context ID + */ + public function deleteByContextId($contextId) + { + $genres = $this->getByContextId($contextId); + while ($genre = $genres->next()) { + $this->update('DELETE FROM genre_settings WHERE genre_id = ?', [(int) $genre->getId()]); + } + $this->update( + 'DELETE FROM genres WHERE context_id = ?', + [(int) $contextId] + ); + } + + /** + * Install default data for settings. + * + * @param int $contextId Context ID + * @param array $locales List of locale codes + */ + public function installDefaults($contextId, $locales) + { + $xmlDao = new XMLDAO(); + $data = $xmlDao->parseStruct('registry/genres.xml', ['genre']); + if (!isset($data['genre'])) { + return false; + } + $seq = 0; + + foreach ($data['genre'] as $entry) { + $attrs = $entry['attributes']; + // attempt to retrieve an installed Genre with this key. + // Do this to preserve the genreId. + $genre = $this->getByKey($attrs['key'], $contextId); + if (!$genre) { + $genre = $this->newDataObject(); + } + $genre->setContextId($contextId); + $genre->setKey($attrs['key']); + $genre->setCategory($attrs['category']); + $genre->setDependent($attrs['dependent']); + $genre->setSupplementary($attrs['supplementary']); + $genre->setRequired((bool) ($attrs['required'] ?? false)); + $genre->setSequence($seq++); + foreach ($locales as $locale) { + $genre->setName(__($attrs['localeKey'], [], $locale), $locale); + } + + if ($genre->getId() > 0) { // existing genre. + $genre->setEnabled(1); + $this->updateObject($genre); + } else { + $this->insertObject($genre); + } + } + } + + /** + * Get default keys. + * + * @return array List of default keys + */ + public function getDefaultKeys() + { + $defaultKeys = []; + $xmlDao = new XMLDAO(); + $data = $xmlDao->parseStruct('registry/genres.xml', ['genre']); + if (isset($data['genre'])) { + foreach ($data['genre'] as $entry) { + $attrs = $entry['attributes']; + $defaultKeys[] = $attrs['key']; + } + } + return $defaultKeys; + } + + /** + * If a key exists for a context. + * + * @param string $key + * @param int $contextId + * @param int $genreId (optional) Current genre to be ignored + * + * @return bool + */ + public function keyExists($key, $contextId, $genreId = null) + { + $params = [$key, (int) $contextId]; + if ($genreId) { + $params[] = (int) $genreId; + } + $result = $this->retrieveRange( + 'SELECT COUNT(*) AS row_count FROM genres WHERE entry_key = ? AND context_id = ?' . (isset($genreId) ? ' AND genre_id <> ?' : ''), + $params + ); + $row = $result->current(); + return $row ? (bool) $row->row_count : false; + } + + /** + * Remove all settings associated with a locale + * + * @param string $locale Locale code + */ + public function deleteSettingsByLocale($locale) + { + $this->update('DELETE FROM genre_settings WHERE locale = ?', [$locale]); + } +} + +if (!PKP_STRICT_MODE) { + class_alias('\PKP\submission\GenreDAO', '\GenreDAO'); +} + diff --git a/classes/submission/genre/Repository.php b/classes/submission/genre/Repository.php new file mode 100644 index 00000000000..f8ab9d9dba1 --- /dev/null +++ b/classes/submission/genre/Repository.php @@ -0,0 +1,192 @@ +where('enabled', true) + ->orderBy('seq'); + + if ($rangeInfo) { + return $query->paginate($rangeInfo->perPage, ['*'], 'page', $rangeInfo->currentPage); + } + + return $query->get(); + } + /** + * Retrieve genres based on whether they are dependent or not. + * + * @param bool $dependent + * @param int $contextId + * @return Collection|Genre[] + */ + public function getByDependenceAndContextId(bool $dependent, int $contextId): Collection + { + return Genre::where('context_id', $contextId) + ->where('dependent', $dependent) + ->where('enabled', true) + ->orderBy('seq') + ->get(); + } + + /** + * Retrieve genres based on supplementary criteria within a specific context. + * + * @param bool $supplementaryFilesOnly + * @param int $contextId + * @return Collection|Genre[] + */ + public function getBySupplementaryAndContextId(bool $supplementaryFilesOnly, int $contextId): Collection + { + return Genre::where('context_id', $contextId) + ->where('supplementary', $supplementaryFilesOnly) + ->orderBy('seq') + ->get(); + } + + /** + * Retrieve genres that are neither supplementary nor dependent. + * + * @param int $contextId + * @return Collection|Genre[] + */ + public function getPrimaryByContextId(int $contextId): Collection + { + return Genre::where('context_id', $contextId) + ->where('dependent', false) + ->where('supplementary', false) + ->where('enabled', true) + ->orderBy('seq') + ->get(); + } + + /** + * Retrieve genres that are required for a new submission in a specific context. + * + * @param int $contextId + * @return Collection|Genre[] + */ + public function getRequiredToSubmit(int $contextId): Collection + { + return Genre::where('context_id', $contextId) + ->where('required', true) + ->get(); + } + + /** + * Check if a genre key exists within a context + * + * @param string $key Key to check. + * @param int $contextId Context ID. + * @param int|null $genreId Genre ID to exclude (optional). + * @return bool True if key exists, otherwise false. + */ + public function keyExists(string $key, int $contextId, ?int $genreId = null): bool + { + $query = Genre::where('entry_key', $key) + ->where('context_id', $contextId); + + if ($genreId !== null) { + $query->where('genre_id', '<>', $genreId); + } + + return $query->exists(); + } + + /** + * Retrieves the genre associated with a specific entry key, optionally within a context. + * + * @param string $key + * @param int|null $contextId + * @return Genre|null + */ + public function getByKey(string $key, ?int $contextId = null) + { + $query = Genre::where('entry_key', $key); + if ($contextId !== null) { + $query->where('context_id', $contextId); + } + return $query->first(); + } + + /** + * Get default keys used in the system. + * + * @return array + */ + public function getDefaultKeys(): array + { + $defaultKeys = []; + $xmlDao = new XMLDAO(); + $data = $xmlDao->parseStruct('registry/genres.xml', ['genre']); + if (isset($data['genre'])) { + foreach ($data['genre'] as $entry) { + $attrs = $entry['attributes']; + $defaultKeys[] = $attrs['key']; + } + } + return $defaultKeys; + } + + /** + * Install default genres based on a predefined set of attributes and context. + * Implementation pending on handling of genre settings. + * + * @param int $contextId + * @param array $locales + */ + public function installDefaults(int $contextId, array $locales) + { + // TODO: Implement once settings handling is completed. + } + + /** + * Get a list of field names for which data is localized. + * Implementation pending on handling of genre settings. + * + * @return array + */ + public function getLocaleFieldNames() + { + // TODO: Implement based on settings table management. + return []; + } + + /** + * Update the locale-specific settings for a genre. + * Implementation pending on handling of genre settings. + * + * @param Genre $genre + */ + public function updateLocaleFields(Genre $genre) + { + // TODO: Define how locale fields should be updated. + } + + /** + * Delete settings associated with a specific locale. + * Functionality related to settings cleanup based on locale. + * + * @param string $locale + */ + public function deleteSettingsByLocale(string $locale) + { + // TODO: Implement deletion logic + } + +}