diff --git a/classes/citation/.project/biblatex-cheatsheet.pdf b/classes/citation/.project/biblatex-cheatsheet.pdf new file mode 100644 index 00000000000..e0c10d943cf Binary files /dev/null and b/classes/citation/.project/biblatex-cheatsheet.pdf differ diff --git a/classes/citation/.project/readme.md b/classes/citation/.project/readme.md new file mode 100644 index 00000000000..e3543024742 --- /dev/null +++ b/classes/citation/.project/readme.md @@ -0,0 +1 @@ +https://tug.ctan.org/info/biblatex-cheatsheet/biblatex-cheatsheet.pdf diff --git a/classes/citation/Citation.php b/classes/citation/Citation.php index 1b1cf80db81..ad70c5f9529 100644 --- a/classes/citation/Citation.php +++ b/classes/citation/Citation.php @@ -7,8 +7,8 @@ /** * @file classes/citation/Citation.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 Citation @@ -20,29 +20,33 @@ namespace PKP\citation; -class Citation extends \PKP\core\DataObject +use PKP\core\DataObject; + +class Citation extends DataObject { /** * Constructor. * - * @param string $rawCitation an unparsed citation string + * @param string|null $rawCitation an unparsed citation string */ - public function __construct($rawCitation = null) + public function __construct(string $rawCitation = null) { parent::__construct(); $this->setRawCitation($rawCitation); } - // - // Getters and Setters - // + /** + * Get publication id. + */ + public function getPublicationId() + { + return $this->getData('publicationId'); + } /** - * Replace URLs through HTML links, if the citation does not already contain HTML links - * - * @return string + * Replace URLs through HTML links, if the citation does not already contain HTML links. */ - public function getCitationWithLinks() + public function getCitationWithLinks(): string { $citation = $this->getRawCitation(); if (stripos($citation, '_cleanCitationString($rawCitation ?? ''); $this->setData('rawCitation', $rawCitation); @@ -80,31 +82,24 @@ public function setRawCitation(?string $rawCitation) /** * Get the sequence number - * - * @return int */ - public function getSequence() + public function getSequence(): int { return $this->getData('seq'); } /** * Set the sequence number - * - * @param int $seq */ - public function setSequence($seq) + public function setSequence(int $seq): void { $this->setData('seq', $seq); } - // - // Private methods - // /** * Take a citation string and clean/normalize it */ - public function _cleanCitationString(string $citationString) : string + private function cleanCitationString(string $citationString = null): string { // 1) Strip slashes and whitespace $citationString = trim(stripslashes($citationString)); diff --git a/classes/citation/CitationDAO.php b/classes/citation/CitationDAO.php deleted file mode 100644 index 0a702d1b4f9..00000000000 --- a/classes/citation/CitationDAO.php +++ /dev/null @@ -1,253 +0,0 @@ -getSequence(); - if (!(is_numeric($seq) && $seq > 0)) { - // Find the latest sequence number - $result = $this->retrieve( - 'SELECT MAX(seq) AS lastseq FROM citations - WHERE publication_id = ?', - [(int)$citation->getData('publicationId')] - ); - $row = $result->current(); - $citation->setSequence($row ? $row->lastseq + 1 : 1); - } - - $this->update( - sprintf('INSERT INTO citations - (publication_id, raw_citation, seq) - VALUES - (?, ?, ?)'), - [ - (int) $citation->getData('publicationId'), - $citation->getRawCitation(), - (int) $seq - ] - ); - $citation->setId($this->getInsertId()); - $this->_updateObjectMetadata($citation); - return $citation->getId(); - } - - /** - * Retrieve a citation by id. - * - * @param int $citationId - * - * @return ?Citation - */ - public function getById($citationId) - { - $result = $this->retrieve( - 'SELECT * FROM citations WHERE citation_id = ?', - [$citationId] - ); - $row = $result->current(); - return $row ? $this->_fromRow((array) $row) : null; - } - - /** - * Import citations from a raw citation list of the particular publication. - * - * @param int $publicationId - * @param string $rawCitationList - * - * @hook CitationDAO::afterImportCitations [[$publicationId, $existingCitations, $importedCitations]] - */ - public function importCitations($publicationId, $rawCitationList) - { - assert(is_numeric($publicationId)); - $publicationId = (int) $publicationId; - - $existingCitations = $this->getByPublicationId($publicationId)->toAssociativeArray(); - - // Remove existing citations. - $this->deleteByPublicationId($publicationId); - - // Tokenize raw citations - $citationTokenizer = new CitationListTokenizerFilter(); - $citationStrings = $citationTokenizer->execute($rawCitationList); - - // Instantiate and persist citations - $importedCitations = []; - if (is_array($citationStrings)) { - foreach ($citationStrings as $seq => $citationString) { - if (!empty(trim($citationString))) { - $citation = new Citation($citationString); - // Set the publication - $citation->setData('publicationId', $publicationId); - // Set the counter - $citation->setSequence($seq + 1); - - $this->insertObject($citation); - - $importedCitations[] = $citation; - } - } - } - - Hook::call('CitationDAO::afterImportCitations', [$publicationId, $existingCitations, $importedCitations]); - } - - /** - * Retrieve an array of citations matching a particular publication id. - * - * @param int $publicationId - * @param ?\PKP\db\DBResultRange $rangeInfo - * - * @return DAOResultFactory containing matching Citations - */ - public function getByPublicationId($publicationId, $rangeInfo = null) - { - $result = $this->retrieveRange( - 'SELECT * - FROM citations - WHERE publication_id = ? - ORDER BY seq, citation_id', - [(int)$publicationId], - $rangeInfo - ); - return new DAOResultFactory($result, $this, '_fromRow', ['id']); - } - - /** - * Update an existing citation. - * - * @param Citation $citation - */ - public function updateObject($citation) - { - $returner = $this->update( - 'UPDATE citations - SET publication_id = ?, - raw_citation = ?, - seq = ? - WHERE citation_id = ?', - [ - (int) $citation->getData('publicationId'), - $citation->getRawCitation(), - (int) $citation->getSequence(), - (int) $citation->getId() - ] - ); - $this->_updateObjectMetadata($citation); - } - - /** - * Delete a citation. - * - * @param Citation $citation - * - * @return bool - */ - public function deleteObject($citation) - { - return $this->deleteById($citation->getId()); - } - - /** - * Delete a citation by id. - */ - public function deleteById(int $citationId): int - { - return DB::table('citations') - ->where('citation_id', '=', $citationId) - ->delete(); - } - - /** - * Delete all citations matching a particular publication id. - * - * @param int $publicationId - * - * @return bool - */ - public function deleteByPublicationId($publicationId) - { - $citations = $this->getByPublicationId($publicationId); - while ($citation = $citations->next()) { - $this->deleteById($citation->getId()); - } - return true; - } - - // - // Private helper methods - // - /** - * Construct a new citation object. - * - * @return Citation - */ - public function _newDataObject() - { - return new Citation(); - } - - /** - * Internal function to return a citation object from a - * row. - * - * @param array $row - * - * @return Citation - */ - public function _fromRow($row) - { - $citation = $this->_newDataObject(); - $citation->setId((int)$row['citation_id']); - $citation->setData('publicationId', (int)$row['publication_id']); - $citation->setRawCitation($row['raw_citation']); - $citation->setSequence((int)$row['seq']); - - $this->getDataObjectSettings('citation_settings', 'citation_id', $row['citation_id'], $citation); - - return $citation; - } - - /** - * Update the citation meta-data - * - * @param Citation $citation - */ - public function _updateObjectMetadata($citation) - { - $this->updateDataObjectSettings('citation_settings', $citation, ['citation_id' => $citation->getId()]); - } -} - -if (!PKP_STRICT_MODE) { - class_alias('\PKP\citation\CitationDAO', '\CitationDAO'); -} diff --git a/classes/citation/Collector.php b/classes/citation/Collector.php new file mode 100644 index 00000000000..f0ba683a33c --- /dev/null +++ b/classes/citation/Collector.php @@ -0,0 +1,123 @@ +dao = $dao; + } + + public function getCount(): int + { + return $this->dao->getCount($this); + } + + /** + * @return Collection + */ + public function getIds(): Collection + { + return $this->dao->getIds($this); + } + + /** + * @copydoc DAO::getMany() + * + * @return LazyCollection + */ + public function getMany(): LazyCollection + { + return $this->dao->getMany($this); + } + + /** + * Filter by publications + */ + public function filterByPublicationIds(?array $publicationIds): self + { + $this->publicationIds = $publicationIds; + return $this; + } + + /** + * Limit the number of objects retrieved + */ + public function limit(?int $count): self + { + $this->count = $count; + return $this; + } + + /** + * Offset the number of objects retrieved, for example to + * retrieve the second page of contents + */ + public function offset(?int $offset): self + { + $this->offset = $offset; + return $this; + } + + /** + * @copydoc CollectorInterface::getQueryBuilder() + */ + public function getQueryBuilder(): Builder + { + $qb = DB::table($this->dao->table . ' as a') + ->select('a.*'); + + if (!is_null($this->count)) { + $qb->limit($this->count); + } + + if (!is_null($this->offset)) { + $qb->offset($this->offset); + } + + if (!is_null($this->publicationIds)) { + $qb->whereIn('a.publication_id', $this->publicationIds); + } + + // Add app-specific query statements + Hook::call('Citation::Collector', [&$qb, $this]); + + return $qb; + } +} diff --git a/classes/citation/DAO.php b/classes/citation/DAO.php new file mode 100644 index 00000000000..b0002487a6f --- /dev/null +++ b/classes/citation/DAO.php @@ -0,0 +1,223 @@ + + */ +class DAO extends EntityDAO +{ + use EntityWithParent; + + /** @copydoc EntityDAO::$schema */ + public $schema = PKPSchemaService::SCHEMA_CITATION; + + /** @copydoc EntityDAO::$table */ + public $table = 'citations'; + + /** @copydoc EntityDAO::$settingsTable */ + public $settingsTable = 'citation_settings'; + + /** @copydoc EntityDAO::$primaryKeyColumn */ + public $primaryKeyColumn = 'citation_id'; + + /** @copydoc EntityDAO::$primaryTableColumns */ + public $primaryTableColumns = [ + 'id' => 'citation_id', + 'publicationId' => 'publication_id', + 'rawCitation' => 'raw_citation', + 'seq' => 'seq' + ]; + + /** + * Get the parent object ID column name + */ + public function getParentColumn(): string + { + return 'publication_id'; + } + + /** + * Instantiate a new DataObject + */ + public function newDataObject(): Citation + { + return App::make(Citation::class); + } + + /** + * Check if a citation exists. + */ + public function exists(int $id, ?int $publicationId = null): bool + { + return DB::table($this->table) + ->where($this->primaryKeyColumn, '=', $id) + ->when($publicationId !== null, + fn(Builder $query) => $query->where($this->getParentColumn(), $publicationId) + ) + ->exists(); + } + + /** + * Get the number of Citation's matching the configured query + */ + public function getCount(Collector $query): int + { + return $query + ->getQueryBuilder() + ->getCountForPagination(); + } + + /** + * Get a list of ids matching the configured query + * + * @return Collection + */ + public function getIds(Collector $query): Collection + { + return $query + ->getQueryBuilder() + ->select('a.' . $this->primaryKeyColumn) + ->pluck('a.' . $this->primaryKeyColumn); + } + + /** + * Get a collection of citations matching the configured query + * + * @return LazyCollection + */ + public function getMany(Collector $query): LazyCollection + { + $rows = $query + ->getQueryBuilder() + ->get(); + + return LazyCollection::make(function () use ($rows) { + foreach ($rows as $row) { + yield $row->citation_id => $this->fromRow($row); + } + }); + } + + /** + * @copydoc EntityDAO::fromRow() + */ + public function fromRow(object $row): Citation + { + return parent::fromRow($row); + } + + /** + * @copydoc EntityDAO::insert() + */ + public function insert(Citation $citation): int + { + return parent::_insert($citation); + } + + /** + * @copydoc EntityDAO::update() + */ + public function update(Citation $citation): void + { + parent::_update($citation); + } + + /** + * @copydoc EntityDAO::delete() + */ + public function delete(Citation $citation): void + { + parent::_delete($citation); + } + + /** + * Delete publication's citations. + */ + public function deleteByPublicationId(int $publicationId): void + { + DB::table($this->table) + ->Where($this->getParentColumn(), '=', $publicationId) + ->delete(); + } + + /** + * Insert on duplicate update. + */ + public function updateOrInsert(Citation $citation): void + { + if (empty($citation->getId())) { + $this->insert($citation); + } else { + $this->update($citation); + } + } + + /** + * Import citations from a raw citation list of the particular publication. + * + * @hook \PKP\citation\DAO::afterImportCitations [[$publicationId, $existingCitations, $importedCitations]] + */ + public function importCitations(int $publicationId, string $rawCitationList): void + { + assert(is_numeric($publicationId)); + $publicationId = (int)$publicationId; + + $existingCitations = Repo::citation()->getByPublicationId($publicationId); + + // Remove existing citations. + $this->deleteByPublicationId($publicationId); + + // Tokenize raw citations + $citationTokenizer = new CitationListTokenizerFilter(); + $citationStrings = $citationTokenizer->execute($rawCitationList); + + // Instantiate and persist citations + $importedCitations = []; + if (is_array($citationStrings)) { + foreach ($citationStrings as $seq => $citationString) { + if (!empty(trim($citationString))) { + $citation = new Citation($citationString); + // Set the publication + $citation->setData('publicationId', $publicationId); + // Set the counter + $citation->setSequence($seq + 1); + + $this->insertObject($citation); + + $importedCitations[] = $citation; + } + } + } + + Hook::call('Citation::DAO::afterImportCitations', [$publicationId, $existingCitations, $importedCitations]); + } +} diff --git a/classes/citation/Repository.php b/classes/citation/Repository.php new file mode 100644 index 00000000000..a4d7f15f3b7 --- /dev/null +++ b/classes/citation/Repository.php @@ -0,0 +1,219 @@ + */ + protected PKPSchemaService $schemaService; + + public function __construct(DAO $dao, Request $request, PKPSchemaService $schemaService) + { + $this->dao = $dao; + $this->request = $request; + $this->schemaService = $schemaService; + } + + /** @copydoc DAO::newDataObject() */ + public function newDataObject(array $params = []): Citation + { + $object = $this->dao->newDataObject(); + if (!empty($params)) { + $object->setAllData($params); + } + return $object; + } + + /** @copydoc DAO::exists() */ + public function exists(int $id, ?int $publicationId = null): bool + { + return $this->dao->exists($id, $publicationId); + } + + /** @copydoc DAO::get() */ + public function get(int $id, ?int $contextId = null): ?Citation + { + return $this->dao->get($id, $contextId); + } + + /** @copydoc DAO::getCollector() */ + public function getCollector(): Collector + { + return App::make(Collector::class); + } + + /** + * Get an instance of the map class for mapping citations to their schema. + */ + public function getSchemaMap(): maps\Schema + { + return app('maps')->withExtensions($this->schemaMap); + } + + /** + * Validate properties for a citation + * + * Perform validation checks on data used to add or edit a citation. + * + * @param Citation|null $object Citation being edited. Pass `null` if creating a new submission + * @param array $props A key/value array with the new data to validate + * @param array $allowedLocales The context's supported locales + * @param string $primaryLocale The context's primary locale + * + * @return array A key/value array with validation errors. Empty if no errors + * + * @hook Citation::validate [[&$errors, $object, $props, $allowedLocales, $primaryLocale]] + */ + public function validate(?Citation $object, array $props, array $allowedLocales, string $primaryLocale): array + { + $errors = []; + + $validator = ValidatorFactory::make( + $props, + $this->schemaService->getValidationRules($this->dao->schema, $allowedLocales) + ); + + // Check required fields if we're adding a citation + ValidatorFactory::required( + $validator, + $object, + $this->schemaService->getRequiredProps($this->dao->schema), + $this->schemaService->getMultilingualProps($this->dao->schema), + $allowedLocales, + $primaryLocale + ); + + // Check for input from disallowed locales + ValidatorFactory::allowedLocales($validator, $this->schemaService->getMultilingualProps($this->dao->schema), $allowedLocales); + + if ($validator->fails()) { + $errors = $this->schemaService->formatValidationErrors($validator->errors()); + } + + Hook::call('Citation::validate', [&$errors, $object, $props, $allowedLocales, $primaryLocale]); + + return $errors; + } + + /** @copydoc DAO::insert() */ + public function add(Citation $row): int + { + $id = $this->dao->insert($row); + Hook::call('Citation::add', [$row]); + return $id; + } + + /** @copydoc DAO::update() */ + public function edit(Citation $row, array $params): void + { + $newRow = clone $row; + $newRow->setAllData(array_merge($newRow->_data, $params)); + Hook::call('Citation::edit', [$newRow, $row, $params]); + $this->dao->update($newRow); + } + + /** @copydoc DAO::delete() */ + public function delete(Citation $citation): void + { + Hook::call('Citation::delete::before', [$citation]); + $this->dao->delete($citation); + Hook::call('Citation::delete', [$citation]); + } + + /** + * Delete a collection of citations + */ + public function deleteMany(Collector $collector): void + { + foreach ($collector->getMany() as $citation) { + $this->delete($citation); + } + } + + /** + * Get all citations for a given publication. + */ + public function getByPublicationId(int $publicationId): array + { + return iterator_to_array($this->getCollector() + ->filterByPublicationIds([$publicationId]) + ->getMany()); + } + + /** + * Delete a publication's citations. + */ + public function deleteByPublicationId(int $publicationId): void + { + $this->dao->deleteByPublicationId($publicationId); + } + + /** + * Import citations from a raw citation list of the particular publication. + * + * @hook Citation::DAO::afterImportCitations [[$publicationId, $existingCitations, $importedCitations]] + */ + public function importCitations(int $publicationId, string $rawCitationList): void + { + assert(is_numeric($publicationId)); + $publicationId = (int)$publicationId; + + $existingCitations = Repo::citation()->getByPublicationId($publicationId); + + // Remove existing citations. + $this->deleteByPublicationId($publicationId); + + // Tokenize raw citations + $citationTokenizer = new CitationListTokenizerFilter(); + $citationStrings = $citationTokenizer->execute($rawCitationList); + + // Instantiate and persist citations + $importedCitations = []; + if (is_array($citationStrings)) { + foreach ($citationStrings as $seq => $citationString) { + if (!empty(trim($citationString))) { + $citation = new Citation($citationString); + // Set the publication + $citation->setData('publicationId', $publicationId); + // Set the counter + $citation->setSequence($seq + 1); + + $this->dao->insert($citation); + + $importedCitations[] = $citation; + } + } + } + + Hook::call('Citation::DAO::afterImportCitations', [$publicationId, $existingCitations, $importedCitations]); + } +} diff --git a/classes/citation/CitationListTokenizerFilter.php b/classes/citation/filter/CitationListTokenizerFilter.php similarity index 66% rename from classes/citation/CitationListTokenizerFilter.php rename to classes/citation/filter/CitationListTokenizerFilter.php index 0bb560fd401..f245f44f873 100644 --- a/classes/citation/CitationListTokenizerFilter.php +++ b/classes/citation/filter/CitationListTokenizerFilter.php @@ -1,29 +1,26 @@ setDisplayName('Split a reference list into separate citations'); @@ -31,17 +28,8 @@ public function __construct() parent::__construct('primitive::string', 'primitive::string[]'); } - // - // Implement template methods from Filter - // - /** - * @see Filter::process() - * - * @param string $input - * - * @return mixed array - */ - public function &process(&$input) + /** @copy Filter::process() */ + public function &process(&$input): array { // The default implementation assumes that raw citations are // separated with line endings. @@ -63,5 +51,5 @@ public function &process(&$input) } if (!PKP_STRICT_MODE) { - class_alias('\PKP\citation\CitationListTokenizerFilter', '\CitationListTokenizerFilter'); + class_alias('\PKP\citation\filter\CitationListTokenizerFilter', '\CitationListTokenizerFilter'); } diff --git a/classes/citation/maps/Schema.php b/classes/citation/maps/Schema.php new file mode 100644 index 00000000000..87082ff1d2c --- /dev/null +++ b/classes/citation/maps/Schema.php @@ -0,0 +1,94 @@ +mapByProperties($this->getProps(), $item); + } + + /** + * Summarize a citation + * + * Includes properties with the apiSummary flag in the citation schema. + */ + public function summarize(Citation $item): array + { + return $this->mapByProperties($this->getSummaryProps(), $item); + } + + /** + * Map a collection of Citations + * + * @see self::map + */ + public function mapMany(Enumerable $collection): Enumerable + { + $this->collection = $collection; + return $collection->map(function ($item) { + return $this->map($item); + }); + } + + /** + * Summarize a collection of Citations + * + * @see self::summarize + */ + public function summarizeMany(Enumerable $collection): Enumerable + { + $this->collection = $collection; + return $collection->map(function ($item) { + return $this->summarize($item); + }); + } + + /** + * Map schema properties of a Citation to an assoc array + */ + protected function mapByProperties(array $props, Citation $item): array + { + $output = []; + foreach ($props as $prop) { + switch ($prop) { + default: + $output[$prop] = $item->getData($prop); + break; + } + } + $output = $this->schemaService->addMissingMultilingualValues($this->schema, $output, $this->context->getSupportedFormLocales()); + ksort($output); + return $this->withExtensions($output, $item); + } +} diff --git a/classes/core/PKPApplication.php b/classes/core/PKPApplication.php index 4b261a5caf3..e301e51db61 100644 --- a/classes/core/PKPApplication.php +++ b/classes/core/PKPApplication.php @@ -499,7 +499,6 @@ public function getDAOMap(): array { return [ 'AnnouncementTypeDAO' => 'PKP\announcement\AnnouncementTypeDAO', - 'CitationDAO' => 'PKP\citation\CitationDAO', 'DataObjectTombstoneDAO' => 'PKP\tombstone\DataObjectTombstoneDAO', 'DataObjectTombstoneSettingsDAO' => 'PKP\tombstone\DataObjectTombstoneSettingsDAO', 'FilterDAO' => 'PKP\filter\FilterDAO', diff --git a/classes/facades/Repo.php b/classes/facades/Repo.php index 8623b3b921f..95c4f1923b7 100644 --- a/classes/facades/Repo.php +++ b/classes/facades/Repo.php @@ -28,6 +28,7 @@ use PKP\author\Repository as AuthorRepository; use PKP\category\Repository as CategoryRepository; use PKP\controlledVocab\Repository as ControlledVocabRepository; +use PKP\citation\Repository as CitationRepository; use PKP\decision\Repository as DecisionRepository; use PKP\emailTemplate\Repository as EmailTemplateRepository; use PKP\highlight\Repository as HighlightRepository; @@ -58,6 +59,11 @@ public static function author(): AuthorRepository return app(AuthorRepository::class); } + public static function citation(): CitationRepository + { + return app(CitationRepository::class); + } + public static function decision(): DecisionRepository { return app()->make(DecisionRepository::class); diff --git a/classes/publication/DAO.php b/classes/publication/DAO.php index a8074a2df1d..cc1b1012f9b 100644 --- a/classes/publication/DAO.php +++ b/classes/publication/DAO.php @@ -3,8 +3,8 @@ /** * @file classes/publication/DAO.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 DAO @@ -21,7 +21,6 @@ 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; @@ -48,17 +47,12 @@ class DAO extends EntityDAO /** @copydoc EntityDAO::$primaryKeyColumn */ public $primaryKeyColumn = 'publication_id'; - /** @var CitationDAO */ - public $citationDao; - /** * Constructor */ public function __construct(CitationDAO $citationDao, PKPSchemaService $schemaService) { parent::__construct($schemaService); - - $this->citationDao = $citationDao; } /** @@ -470,7 +464,7 @@ protected function deleteCategories(int $publicationId): void */ protected function saveCitations(Publication $publication) { - $this->citationDao->importCitations($publication->getId(), $publication->getData('citationsRaw')); + Repo::citation()->importCitations($publication->getId(), $publication->getData('citationsRaw')); } /** @@ -478,7 +472,7 @@ protected function saveCitations(Publication $publication) */ protected function deleteCitations(int $publicationId) { - $this->citationDao->deleteByPublicationId($publicationId); + Repo::citation()->deleteByPublicationId($publicationId); } /** diff --git a/classes/publication/Repository.php b/classes/publication/Repository.php index 69a04a52af3..a8a324d5ffa 100644 --- a/classes/publication/Repository.php +++ b/classes/publication/Repository.php @@ -1,9 +1,10 @@ getData('citationsRaw'))) { - $citationDao = DAORegistry::getDAO('CitationDAO'); /** @var \PKP\citation\CitationDAO $citationDao */ - $citationDao->importCitations($newPublication->getId(), $newPublication->getData('citationsRaw')); + Repo::citation()->importCitations($newPublication->getId(), $newPublication->getData('citationsRaw')); } $genreDao = DAORegistry::getDAO('GenreDAO'); diff --git a/classes/publication/maps/Schema.php b/classes/publication/maps/Schema.php index 5b56bfb1f25..095097dcb75 100644 --- a/classes/publication/maps/Schema.php +++ b/classes/publication/maps/Schema.php @@ -2,8 +2,8 @@ /** * @file classes/publication/maps/Schema.php * - * Copyright (c) 2014-2020 Simon Fraser University - * Copyright (c) 2000-2020 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 Schema @@ -18,7 +18,6 @@ use APP\publication\Publication; use APP\submission\Submission; use Illuminate\Support\Enumerable; -use PKP\citation\CitationDAO; use PKP\context\Context; use PKP\db\DAORegistry; use PKP\services\PKPSchemaService; @@ -135,12 +134,11 @@ protected function mapByProperties(array $props, Publication $publication, bool $output[$prop] = $publication->getData('categoryIds'); break; case 'citations': - $citationDao = DAORegistry::getDAO('CitationDAO'); /** @var CitationDAO $citationDao */ $output[$prop] = array_map( function ($citation) { return $citation->getCitationWithLinks(); }, - $citationDao->getByPublicationId($publication->getId())->toArray() + Repo::citation()->getByPublicationId($publication->getId()) ); break; case 'doiObject': diff --git a/classes/services/PKPSchemaService.php b/classes/services/PKPSchemaService.php index 0ba1d033890..92e2fa4b768 100644 --- a/classes/services/PKPSchemaService.php +++ b/classes/services/PKPSchemaService.php @@ -2,8 +2,8 @@ /** * @file classes/services/PKPSchemaService.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 PKPSchemaService @@ -31,6 +31,7 @@ class PKPSchemaService public const SCHEMA_ANNOUNCEMENT = 'announcement'; public const SCHEMA_AUTHOR = 'author'; public const SCHEMA_CATEGORY = 'category'; + public const SCHEMA_CITATION = 'citation'; public const SCHEMA_CONTEXT = 'context'; public const SCHEMA_DOI = 'doi'; public const SCHEMA_DECISION = 'decision'; diff --git a/plugins/importexport/native/filter/NativeXmlPKPPublicationFilter.php b/plugins/importexport/native/filter/NativeXmlPKPPublicationFilter.php index db84fff1e15..b9c3de8e666 100644 --- a/plugins/importexport/native/filter/NativeXmlPKPPublicationFilter.php +++ b/plugins/importexport/native/filter/NativeXmlPKPPublicationFilter.php @@ -3,8 +3,8 @@ /** * @file plugins/importexport/native/filter/NativeXmlPKPPublicationFilter.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 NativeXmlPKPPublicationFilter @@ -19,7 +19,6 @@ use APP\core\Application; use APP\facades\Repo; use APP\publication\Publication; -use PKP\citation\CitationDAO; use PKP\controlledVocab\ControlledVocab; use PKP\db\DAORegistry; use PKP\filter\Filter; @@ -286,8 +285,7 @@ public function parseCitations($n, $publication) $citationsString .= $nodeText . "\n"; } $publication->setData('citationsRaw', $citationsString); - $citationDao = DAORegistry::getDAO('CitationDAO'); /** @var CitationDAO $citationDao */ - $citationDao->importCitations($publicationId, $citationsString); + Repo::citation()->importCitations($publicationId, $citationsString); } // diff --git a/plugins/importexport/native/filter/PKPPublicationNativeXmlFilter.php b/plugins/importexport/native/filter/PKPPublicationNativeXmlFilter.php index 7c0bfd346b9..eaec716a4d3 100644 --- a/plugins/importexport/native/filter/PKPPublicationNativeXmlFilter.php +++ b/plugins/importexport/native/filter/PKPPublicationNativeXmlFilter.php @@ -3,8 +3,8 @@ /** * @file plugins/importexport/native/filter/PKPPublicationNativeXmlFilter.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 PKPPublicationNativeXmlFilter @@ -21,7 +21,6 @@ use APP\plugins\importexport\native\NativeImportExportDeployment; use APP\publication\Publication; use Exception; -use PKP\citation\CitationDAO; use PKP\controlledVocab\ControlledVocab; use PKP\db\DAORegistry; use PKP\filter\FilterGroup; @@ -341,10 +340,8 @@ public function getFiles($representation) */ private function createCitationsNode($doc, $deployment, $publication) { - $citationDao = DAORegistry::getDAO('CitationDAO'); /** @var CitationDAO $citationDao */ - $nodeCitations = $doc->createElementNS($deployment->getNamespace(), 'citations'); - $submissionCitations = $citationDao->getByPublicationId($publication->getId())->toAssociativeArray(); + $submissionCitations = Repo::citation()->getByPublicationId($publication->getId()); foreach ($submissionCitations as $submissionCitation) { $rawCitation = $submissionCitation->getRawCitation(); diff --git a/schemas/citation.json b/schemas/citation.json new file mode 100644 index 00000000000..5e559c16f38 --- /dev/null +++ b/schemas/citation.json @@ -0,0 +1,611 @@ +{ + "title": "Citation", + "description": "Affiliation is an institution with which an author is associated.", + "required": [ + "editor", + "title", + "date", + "publicationId" + ], + "properties": { + "id": { + "type": "integer", + "apiSummary": true + }, + "publicationId": { + "type": "integer", + "apiSummary": true + }, + "abstract": { + "type": "string", + "description": "Abstract of this work", + "multilingual": true, + "apiSummary": true, + "validation": [] + }, + "addendum": { + "type": "string", + "description": "Addendum to this work", + "multilingual": false, + "apiSummary": true, + "validation": [] + }, + "afterword": { + "type": "array", + "description": "Author(s) of afterword.", + "multilingual": false, + "apiSummary": true, + "items": { + "type": "object", + "$ref": "#/definitions/Author" + } + }, + "annotation": { + "type": "string", + "description": "The annotations of this work.", + "multilingual": false, + "apiSummary": true, + "validation": [] + }, + "annotator": { + "type": "array", + "description": "Author(s) of annotations.", + "multilingual": false, + "apiSummary": true, + "items": { + "type": "object", + "$ref": "#/definitions/Author" + } + }, + "author": { + "type": "array", + "description": "List of authors.", + "multilingual": false, + "apiSummary": true, + "items": { + "type": "object", + "$ref": "#/definitions/Author" + } + }, + "bookauthor": { + "type": "array", + "description": "List of authors.", + "multilingual": false, + "apiSummary": true, + "items": { + "type": "object", + "$ref": "#/definitions/Author" + } + }, + "bookpagination": { + "type": "string", + "description": "The pagination of this work.", + "multilingual": false, + "apiSummary": true, + "validation": [ + "nullable", + "string:'page','column','section','paragraph','verse','line'" + ] + }, + "booksubtitle": { + "type": "string", + "description": "Subtitle of book.", + "multilingual": true, + "apiSummary": true, + "validation": [] + }, + "booktitle": { + "type": "string", + "description": "Title of book.", + "multilingual": true, + "apiSummary": true, + "validation": [] + }, + "booktitleaddon": { + "type": "string", + "description": "Addon title of book.", + "multilingual": true, + "apiSummary": true, + "validation": [] + }, + "chapter": { + "type": "string", + "description": "Chapter number.", + "multilingual": false, + "apiSummary": true, + "validation": [] + }, + "commentator": { + "type": "array", + "description": "Author(s) of commentary.", + "multilingual": false, + "apiSummary": true, + "items": { + "type": "object", + "$ref": "#/definitions/Author" + } + }, + "date": { + "type": "string", + "description": "The publication date", + "multilingual": false, + "apiSummary": true, + "validation": [ + "nullable", + "date_format:Y-m-d" + ] + }, + "doi": { + "type": "string", + "description": "The DOI itself, such as `10.1234/5a6b-7c8d`.", + "multilingual": false, + "apiSummary": true, + "validation": [ + "nullable", + "regex:/^\\d+(.\\d+)+\\//" + ] + }, + "edition": { + "type": "int", + "description": "Edition or issue", + "multilingual": false, + "apiSummary": true, + "validation": [ + "nullable" + ] + }, + "editor": { + "type": "array", + "description": "List of editors.", + "multilingual": false, + "apiSummary": true, + "items": { + "type": "object", + "$ref": "#/definitions/Author" + } + }, + "editora": { + "type": "array", + "description": "List of editors.", + "multilingual": false, + "apiSummary": true, + "items": { + "type": "object", + "$ref": "#/definitions/Author" + } + }, + "editorb": { + "type": "array", + "description": "List of editors.", + "multilingual": false, + "apiSummary": true, + "items": { + "type": "object", + "$ref": "#/definitions/Author" + } + }, + "editorc": { + "type": "array", + "description": "List of editors.", + "multilingual": false, + "apiSummary": true, + "items": { + "type": "object", + "$ref": "#/definitions/Author" + } + }, + "eid": { + "type": "string", + "description": "Electronic identifier of an article", + "multilingual": false, + "apiSummary": true, + "validation": [ + "nullable" + ] + }, + "eprint": { + "type": "string", + "description": "Electronic identifier of an online publication", + "multilingual": false, + "apiSummary": true, + "validation": [] + }, + "eprintclass": { + "type": "string", + "description": "Additional information to an online publication.", + "multilingual": false, + "apiSummary": true, + "validation": [] + }, + "eprinttype": { + "type": "string", + "description": "Eprint identifier type.", + "multilingual": false, + "apiSummary": true, + "validation": [ + "nullable" + ] + }, + "eventdate": { + "type": "string", + "description": "Event date of reference.", + "multilingual": false, + "apiSummary": true, + "validation": [] + }, + "eventtitle": { + "type": "string", + "description": "Event title of reference.", + "multilingual": true, + "apiSummary": true, + "validation": [] + }, + "foreword": { + "type": "array", + "description": "Author(s) of foreword.", + "multilingual": true, + "apiSummary": true, + "items": { + "type": "object", + "$ref": "#/definitions/Author" + } + }, + "holder": { + "type": "string", + "description": "Holder of patent.", + "multilingual": false, + "apiSummary": true, + "validation": [] + }, + "howpublished": { + "type": "string", + "description": "Non-standard publication details.", + "multilingual": false, + "apiSummary": true, + "validation": [] + }, + "institution": { + "type": "string", + "description": "University or similar.", + "multilingual": false, + "apiSummary": true, + "validation": [] + }, + "introduction": { + "type": "array", + "description": "Author(s) of introduction.", + "multilingual": true, + "apiSummary": true, + "items": { + "type": "object", + "$ref": "#/definitions/Author" + } + }, + "isbn": { + "type": "string", + "description": "International Standard Book Number.", + "multilingual": false, + "apiSummary": true, + "validation": [] + }, + "isrn": { + "type": "string", + "description": "International Standard Technical Report Number.", + "multilingual": false, + "apiSummary": true, + "validation": [] + }, + "issn": { + "type": "string", + "description": "International Standard Serial Number.", + "multilingual": false, + "apiSummary": true, + "validation": [] + }, + "issue": { + "type": "string", + "description": "Non-number issue of journal.", + "multilingual": false, + "apiSummary": true, + "validation": [] + }, + "issuesubtitle": { + "type": "string", + "description": "The subtitle of the issue.", + "multilingual": true, + "apiSummary": true, + "validation": [] + }, + "issuetitle": { + "type": "string", + "description": "The title of the issue.", + "multilingual": true, + "apiSummary": true, + "validation": [] + }, + "journalsubtitle": { + "type": "string", + "description": "The subtitle of the journal.", + "multilingual": true, + "apiSummary": true, + "validation": [] + }, + "journaltitle": { + "type": "string", + "description": "The title of the journal.", + "multilingual": true, + "apiSummary": true, + "validation": [] + }, + "keywords": { + "type": "array", + "description": "Keywords", + "multilingual": false, + "apiSummary": true, + "items": { + "type": "string" + } + }, + "language": { + "type": "string", + "description": "The language of this work.", + "multilingual": false, + "apiSummary": true, + "validation": [] + }, + "langid": { + "type": "string", + "description": "The language of bibliography / citation.", + "multilingual": false, + "apiSummary": true, + "validation": [] + }, + "location": { + "type": "string", + "description": "Location or address, where published.", + "multilingual": false, + "apiSummary": true, + "validation": [] + }, + "mainsubtitle": { + "type": "string", + "description": "Subtitle of multi-volume book.", + "multilingual": true, + "apiSummary": true, + "validation": [] + }, + "maintitle": { + "type": "string", + "description": "Title of multi-volume book.", + "multilingual": true, + "apiSummary": true, + "validation": [] + }, + "maintitleaddon": { + "type": "string", + "description": "Addon to title of multi-volume book.", + "multilingual": true, + "apiSummary": true, + "validation": [] + }, + "note": { + "type": "string", + "description": "Notes", + "multilingual": false, + "apiSummary": true, + "validation": [] + }, + "number": { + "type": "string", + "description": "Numbered issue of journal or book in series.", + "multilingual": false, + "apiSummary": true, + "validation": [] + }, + "organization": { + "type": "string", + "description": "Manual/website publisher or event sponsor", + "multilingual": false, + "apiSummary": true, + "validation": [] + }, + "origdate": { + "type": "string", + "description": "Publication date of the original work.", + "multilingual": false, + "apiSummary": true, + "validation": [ + "nullable", + "date_format:Y-m-d" + ] + }, + "origlanguage": { + "type": "string", + "description": "The language of the original work.", + "multilingual": false, + "apiSummary": true, + "validation": [ + "regex:/^([A-Za-z]{2,4})(?[_-]([A-Za-z]{4,5}|[0-9]{4}))?([_-]([A-Za-z]{2}|[0-9]{3}))?(@[a-z]{2,30}(?&sc)?)?$/" + ] + }, + "origlocation": { + "type": "string", + "description": "Location or address, where originally published.", + "multilingual": false, + "apiSummary": true, + "validation": [] + }, + "origpublisher": { + "type": "string", + "description": "Original publisher(s).", + "multilingual": false, + "apiSummary": true, + "validation": [] + }, + "origtitle": { + "type": "string", + "description": "The title of the original work.", + "multilingual": true, + "apiSummary": true, + "validation": [] + }, + "pages": { + "type": "array", + "description": "The number of pages of the work/article, e.g. 4, 46-53", + "multilingual": false, + "apiSummary": true, + "items": { + "type": "string" + } + }, + "pagetotal": { + "type": "string", + "description": "Total number of pages.", + "multilingual": false, + "apiSummary": true, + "validation": [] + }, + "pagination": { + "type": "string", + "description": "The pagination of this work.", + "multilingual": false, + "apiSummary": true, + "validation": [ + "nullable", + "string:'page','column','section','paragraph','verse','line'" + ] + }, + "part": { + "type": "string", + "description": "Number of physical part of logical volume", + "multilingual": false, + "apiSummary": true, + "validation": [] + }, + "publisher": { + "type": "string", + "description": "Publisher(s).", + "multilingual": false, + "apiSummary": true, + "validation": [] + }, + "pubstate": { + "type": "string", + "description": "The state of this work, e.g. submitted.", + "multilingual": false, + "apiSummary": true, + "validation": [ + "nullable", + "string:'submitted','inpreparation','forthcoming','inpress','prepublished'" + ] + }, + "series": { + "type": "string", + "description": "Name of series", + "multilingual": false, + "apiSummary": true, + "validation": [] + }, + "shorttitle": { + "type": "string", + "description": "The abridged or short title of this work.", + "multilingual": true, + "apiSummary": true, + "validation": [ + "nullable", + "no_new_line" + ] + }, + "subtitle": { + "type": "string", + "description": "Subtitle of work.", + "multilingual": true, + "apiSummary": true, + "validation": [] + }, + "title": { + "type": "string", + "description": "Title of work.", + "multilingual": true, + "apiSummary": true, + "validation": [] + }, + "titleaddon": { + "type": "string", + "description": "Addon title of work.", + "multilingual": true, + "apiSummary": true, + "validation": [] + }, + "translator": { + "type": "array", + "description": "Translator(s) of (work)title.", + "multilingual": false, + "apiSummary": true, + "items": { + "type": "object", + "$ref": "#/definitions/Author" + } + }, + "type": { + "type": "string", + "description": "Addon title of work.", + "multilingual": true, + "apiSummary": true, + "validation": [ + "nullable", + "string:'manual','patent','report','thesis','mathesis','phdthesis','candthesis','techreport','resreport','software','datacd','audiocd'" + ] + }, + "url": { + "type": "string", + "description": "The public URL for this publication or where it will be available if it has not yet been published.", + "multilingual": false, + "apiSummary": true, + "validation": [ + "nullable" + ] + }, + "urldate": { + "type": "string", + "description": "Access date to the url.", + "multilingual": false, + "apiSummary": true, + "validation": [ + "nullable", + "date_format:Y-m-d" + ] + }, + "venue": { + "type": "string", + "description": "Venue of event.", + "multilingual": false, + "apiSummary": true, + "validation": [ + "nullable", + "date_format:Y-m-d" + ] + }, + "version": { + "type": "string", + "description": "Version of this work", + "multilingual": false, + "apiSummary": true, + "validation": [] + }, + "volume": { + "type": "string", + "description": "Volume of journal or multi-volume book.", + "multilingual": false, + "apiSummary": true, + "validation": [] + }, + "volumes": { + "type": "string", + "description": "Number of volumes for multi-volume work.", + "multilingual": false, + "apiSummary": true, + "validation": [] + } + } +} diff --git a/schemas/publication.json b/schemas/publication.json index 2e4864a16ca..52c7035551b 100644 --- a/schemas/publication.json +++ b/schemas/publication.json @@ -57,9 +57,11 @@ "citations": { "type": "array", "description": "Optional metadata that contains an array of references for works cited in this submission. References have been split and parsed from the raw text.", + "apiSummary": true, "readOnly": true, "items": { - "type": "string" + "type": "object", + "$ref": "#/definitions/Citation" } }, "citationsRaw": { diff --git a/tests/classes/citation/CitationListTokenizerFilterTest.php b/tests/classes/citation/CitationListTokenizerFilterTest.php index dd3e802f5c5..b582762cde2 100644 --- a/tests/classes/citation/CitationListTokenizerFilterTest.php +++ b/tests/classes/citation/CitationListTokenizerFilterTest.php @@ -3,22 +3,22 @@ /** * @file tests/classes/citation/CitationListTokenizerFilterTest.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 CitationListTokenizerFilterTest * * @ingroup tests_classes_citation * - * @see CitationListTokenizerFilter + * @see \PKP\citation\filter\CitationListTokenizerFilter * * @brief Test class for CitationListTokenizerFilter. */ namespace PKP\tests\classes\citation; -use PKP\citation\CitationListTokenizerFilter; +use PKP\citation\filter\CitationListTokenizerFilter; use PKP\tests\PKPTestCase; use PHPUnit\Framework\Attributes\CoversClass; diff --git a/tests/classes/publication/PublicationTest.php b/tests/classes/publication/PublicationTest.php index 7045dedcb60..a932253c7b6 100644 --- a/tests/classes/publication/PublicationTest.php +++ b/tests/classes/publication/PublicationTest.php @@ -1,9 +1,10 @@ publication = (new DAO( - new CitationDAO(), new PKPSchemaService() ))->newDataObject(); } @@ -48,7 +47,7 @@ protected function tearDown(): void unset($this->publication); parent::tearDown(); } - + public function testPageArray() { $expected = [['i', 'ix'], ['6', '11'], ['19'], ['21']]; diff --git a/tools/parseCitations.php b/tools/parseCitations.php index 87ba630b6d6..86fa97d9ce8 100644 --- a/tools/parseCitations.php +++ b/tools/parseCitations.php @@ -3,8 +3,8 @@ /** * @file tools/parseCitations.php * - * Copyright (c) 2014-2021 Simon Fraser University - * Copyright (c) 2003-2021 John Willinsky + * Copyright (c) 2014-2024 Simon Fraser University + * Copyright (c) 2003-2024 John Willinsky * Distributed under the GNU GPL v3. For full terms see the file docs/COPYING. * * @class CitationsParsingTool @@ -16,7 +16,6 @@ use APP\core\Application; use APP\facades\Repo; -use PKP\db\DAORegistry; require(dirname(__FILE__, 4) . '/tools/bootstrap.php'); @@ -57,7 +56,6 @@ public function usage() */ public function execute() { - $citationDao = DAORegistry::getDAO('CitationDAO'); $contextDao = Application::getContextDAO(); switch (array_shift($this->parameters)) { @@ -106,11 +104,9 @@ public function execute() */ private function _parseSubmission($submission) { - /** @var CitationDAO */ - $citationDao = DAORegistry::getDAO('CitationDAO'); foreach ($submission->getData('publications') as $publication) { if (!empty($publication->getData('citationsRaw'))) { - $citationDao->importCitations($publication->getId(), $publication->getData('citationsRaw')); + Repo::citation()->dao->importCitations($publication->getId(), $publication->getData('citationsRaw')); } } }