diff --git a/api/v1/dataCitations/PKPDataCitationController.php b/api/v1/dataCitations/PKPDataCitationController.php
new file mode 100644
index 00000000000..686fd5b1f0e
--- /dev/null
+++ b/api/v1/dataCitations/PKPDataCitationController.php
@@ -0,0 +1,224 @@
+whereNumber('publicationId')
+ ->group(function () {
+ Route::get('', $this->getMany(...))
+ ->name('dataCitation.getMany');
+
+ Route::get('{dataCitationId}', $this->get(...))
+ ->name('dataCitation.getDataCitation')
+ ->whereNumber('dataCitationId');
+
+ Route::post('', $this->add(...))
+ ->name('dataCitation.add');
+
+ Route::put('{dataCitationId}', $this->edit(...))
+ ->name('dataCitation.edit')
+ ->whereNumber('dataCitationId');
+
+ Route::delete('{dataCitationId}', $this->delete(...))
+ ->name('dataCitation.delete')
+ ->whereNumber('dataCitationId');
+ });
+ }
+
+ /**
+ * @copydoc \PKP\core\PKPBaseController::authorize()
+ */
+ public function authorize(PKPRequest $request, array &$args, array $roleAssignments): bool
+ {
+ $this->addPolicy(new UserRolesRequiredPolicy($request), true);
+
+ $rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);
+
+ $this->addPolicy(new ContextRequiredPolicy($request));
+
+ foreach ($roleAssignments as $role => $operations) {
+ $rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations));
+ }
+
+ $this->addPolicy($rolePolicy);
+
+ return parent::authorize($request, $args, $roleAssignments);
+ }
+
+ /**
+ * Get a single data dataCitation.
+ */
+ public function get(Request $illuminateRequest): JsonResponse
+ {
+ $announcement = DataCitation::find((int) $illuminateRequest->route('dataCitationId'));
+
+ if (!$dataCitation) {
+ return response()->json([
+ 'error' => __('api.dataCitations.404.dataCitationNotFound')
+ ], Response::HTTP_OK);
+ }
+
+ return response()->json(Repo::dataCitation()->getSchemaMap()->map($dataCitation), Response::HTTP_OK);
+ }
+
+ /**
+ * Get a collection of data citations.
+ *
+ * @hook API::dataCitations::params [[$collector, $illuminateRequest]]
+ */
+ public function getMany(Request $illuminateRequest): JsonResponse
+ {
+ $dataCitations = DataCitation::limit(self::DEFAULT_COUNT)->offset(0);
+
+ if ($illuminateRequest->route('publicationId')) {
+ $dataCitations->withPublicationId($illuminateRequest->route('publicationId'));
+ }
+
+ Hook::run('API::dataCitations::params', [$dataCitations, $illuminateRequest]);
+
+ return response()->json([
+ 'itemsMax' => $dataCitations->count(),
+ 'items' => Repo::dataCitation()->getSchemaMap()->summarizeMany($dataCitations->get())->values(),
+ ], Response::HTTP_OK);
+
+
+ }
+
+ /**
+ * Add a data citation.
+ */
+ public function add(Request $illuminateRequest): JsonResponse
+ {
+ $params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_DATA_CITATION, $illuminateRequest->input());
+ $params['publicationId'] = (int) $illuminateRequest->route('publicationId');
+
+ $errors = Repo::dataCitation()->validate(null, $params);
+ if (!empty($errors)) {
+ return response()->json($errors, Response::HTTP_BAD_REQUEST);
+ }
+
+ $dataCitation = DataCitation::create($params);
+
+ return response()->json(Repo::dataCitation()->getSchemaMap()->map($dataCitation), Response::HTTP_OK);
+ }
+
+ /**
+ * Edit a data citation.
+ */
+ public function edit(Request $illuminateRequest): JsonResponse
+ {
+ $dataCitation = DataCitation::find((int)$illuminateRequest->route('dataCitationId'));
+
+ if (!$dataCitation) {
+ return response()->json([
+ 'error' => __('api.dataCitations.404.dataCitationNotFound'),
+ ], Response::HTTP_NOT_FOUND);
+ }
+
+ $params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_DATA_CITATION, $illuminateRequest->input());
+ $params['id'] = $dataCitation->id;
+
+ $errors = Repo::dataCitation()->validate($dataCitation, $params);
+ if (!empty($errors)) {
+ return response()->json($errors, Response::HTTP_BAD_REQUEST);
+ }
+
+ $dataCitation->update($params);
+
+ $dataCitation = DataCitation::find($dataCitation->id);
+
+ return response()->json(
+ Repo::dataCitation()->getSchemaMap()->map($dataCitation), Response::HTTP_OK
+ );
+ }
+
+ /**
+ * Delete a data citation.
+ */
+ public function delete(Request $illuminateRequest): JsonResponse
+ {
+ $dataCitation = DataCitation::find((int) $illuminateRequest->route('dataCitationId'));
+
+ if (!$dataCitation) {
+ return response()->json([
+ 'error' => __('api.dataCitations.404.dataCitationNotFound')
+ ], Response::HTTP_OK);
+ }
+
+ $dataCitation->delete();
+
+ return response()->json(
+ Repo::dataCitation()->getSchemaMap()->map($dataCitation), Response::HTTP_OK
+ );
+ }
+
+}
diff --git a/api/v1/submissions/PKPSubmissionController.php b/api/v1/submissions/PKPSubmissionController.php
index 5ca5a66e742..400eb481b3a 100644
--- a/api/v1/submissions/PKPSubmissionController.php
+++ b/api/v1/submissions/PKPSubmissionController.php
@@ -43,6 +43,7 @@
use PKP\citation\Citation;
use PKP\citation\enum\CitationProcessingStatus;
use PKP\components\forms\FormComponent;
+use PKP\components\forms\publication\PKPDataAvailabilityAndCitationsForm;
use PKP\components\forms\publication\PKPMetadataForm;
use PKP\components\forms\publication\PKPPublicationIdentifiersForm;
use PKP\components\forms\publication\PKPPublicationLicenseForm;
@@ -120,6 +121,7 @@ class PKPSubmissionController extends PKPBaseController
'editContributor',
'saveContributorsOrder',
'addDecision',
+ 'getPublicationDataAvailabilityAndCitationsForm',
'getPublicationMetadataForm',
'getPublicationIdentifierForm',
'getPublicationLicenseForm',
@@ -325,6 +327,7 @@ public function getGroupRoutes(): void
Route::prefix('{submissionId}/publications/{publicationId}/_components')->group(function () {
Route::get('metadata', $this->getPublicationMetadataForm(...))->name('submission.publication._components.metadata');
+ Route::get('dataAvailabilityAndCitation', $this->getPublicationDataAvailabilityAndCitationsForm(...))->name('submission.publication._components.dataCitation');
Route::get('titleAbstract', $this->getPublicationTitleAbstractForm(...))->name('submission.publication._components.titleAbstract');
Route::get('changeLanguageMetadata', $this->getChangeLanguageMetadata(...))->name('submission.publication._components.changeLanguageMetadata');
})->whereNumber(['submissionId', 'publicationId']);
@@ -424,6 +427,7 @@ public function authorize(PKPRequest $request, array &$args, array $roleAssignme
if (in_array(
$actionName,
[
+ 'getPublicationDataAvailabilityAndCitationsForm',
'getPublicationMetadataForm',
'getPublicationIdentifierForm',
'getPublicationLicenseForm',
@@ -2040,6 +2044,33 @@ protected function getPublicationMetadataForm(Request $illuminateRequest): JsonR
return response()->json($this->getLocalizedForm($metadataForm, $submissionLocale, $locales), Response::HTTP_OK);
}
+ /**
+ * Get Publication Data Citation Form component
+ */
+ protected function getPublicationDataAvailabilityAndCitationsForm(Request $illuminateRequest): JsonResponse
+ {
+ $data = $this->getSubmissionAndPublicationData($illuminateRequest);
+
+ if (isset($data['error'])) {
+ return response()->json([ 'error' => $data['error'],], $data['status']);
+ }
+
+ $context = $data['context']; /** @var Context $context*/
+ $submission = $data['submission']; /** @var Submission $submission */
+ $publication = $data['publication']; /** @var Publication $publication*/
+
+ $publicationApiUrl = $data['publicationApiUrl']; /** @var String $publicationApiUrl*/
+
+ $submissionLocale = $submission->getData('locale');
+ $locales = $this->getPublicationFormLocales($context, $submission);
+ $dataAvailabilitySetting = (bool) $context->getData('dataAvailability');
+
+ $dataAvailabilityAndCitationsForm = new PKPDataAvailabilityAndCitationsForm($publicationApiUrl, $locales, $publication, $dataAvailabilitySetting);
+
+ return response()->json($this->getLocalizedForm($dataAvailabilityAndCitationsForm, $submissionLocale, $locales), Response::HTTP_OK);
+
+ }
+
/**
* Get Publication License Form component
*/
diff --git a/classes/components/forms/context/PKPMetadataSettingsForm.php b/classes/components/forms/context/PKPMetadataSettingsForm.php
index 5589e0f347d..3439f326bc1 100644
--- a/classes/components/forms/context/PKPMetadataSettingsForm.php
+++ b/classes/components/forms/context/PKPMetadataSettingsForm.php
@@ -215,6 +215,19 @@ public function __construct($action, $context)
],
'value' => $context->getData('dataAvailability') ? $context->getData('dataAvailability') : Context::METADATA_DISABLE,
]))
+ ->addField(new FieldMetadataSetting('dataCitations', [
+ 'label' => __('manager.setup.metadata.dataCitations'),
+ 'description' => __('manager.setup.metadata.dataCitations.description'),
+ 'options' => [
+ ['value' => Context::METADATA_ENABLE, 'label' => __('manager.setup.metadata.dataCitations.enable')]
+ ],
+ 'submissionOptions' => [
+ ['value' => Context::METADATA_ENABLE, 'label' => __('manager.setup.metadata.dataCitations.noRequest')],
+ ['value' => Context::METADATA_REQUEST, 'label' => __('manager.setup.metadata.dataCitations.request')],
+ ['value' => Context::METADATA_REQUIRE, 'label' => __('manager.setup.metadata.dataCitations.require')],
+ ],
+ 'value' => $context->getData('dataCitations') ? $context->getData('dataCitations') : Context::METADATA_DISABLE,
+ ]))
->addField(new FieldOptions('submitWithCategories', [
'label' => __('category.category'),
'description' => __('manager.submitWithCategories.description'),
diff --git a/classes/components/forms/dataCitation/DataCitationEditForm.php b/classes/components/forms/dataCitation/DataCitationEditForm.php
new file mode 100644
index 00000000000..e76e2d6ab71
--- /dev/null
+++ b/classes/components/forms/dataCitation/DataCitationEditForm.php
@@ -0,0 +1,99 @@
+action = $action;
+
+ $types = ['DOI', 'Accession', 'PURL', 'ARK', 'URI', 'ARXIV', 'ECLI', 'Handle', 'ISSN', 'ISBN', 'PMID', 'PMCID', 'UUID'];
+ $identifierTypes = array_map(fn($type) => ['value' => $type, 'label' => $type], $types);
+
+ $types = ['supporting', 'generated', 'analyzed', 'non-analyzed'];
+ $relationshipTypes = array_map(
+ fn($type) => [
+ 'value' => $type,
+ 'label' => __('submission.dataCitations.label.relationshipType.' . $type),
+ ],
+ $types
+ );
+
+ $this->addField(new FieldText('title', [
+ 'label' => __('submission.dataCitations.label.title'),
+ 'description' => '',
+ 'value' => null,
+ 'isRequired' => true
+ ]));
+
+ $this->addField(new FieldSelect('identifierType', [
+ 'label' => __('submission.dataCitations.label.identifierType'),
+ 'options' => $identifierTypes
+ ]));
+
+ $this->addField(new FieldText('identifier', [
+ 'label' => __('submission.dataCitations.label.identifier'),
+ 'description' => '',
+ 'value' => null
+ ]));
+
+ $this->addField(new FieldSelect('relationshipType', [
+ 'label' => __('submission.dataCitations.label.relationshipType'),
+ 'options' => $relationshipTypes,
+ 'isRequired' => true
+ ]));
+
+ $this->addField(new FieldText('repository', [
+ 'label' => __('submission.dataCitations.label.repository'),
+ 'description' => '',
+ 'value' => null
+ ]));
+
+ $this->addField(new FieldText('year', [
+ 'label' => __('submission.dataCitations.label.year'),
+ 'description' => '',
+ 'value' => null
+ ]));
+
+ $this->addField(new FieldAuthors('authors', [
+ 'label' => __('submission.dataCitations.label.creators'),
+ 'description' => '',
+ 'value' => null,
+ ]));
+
+ $this->addField(new FieldText('url', [
+ 'label' => __('submission.dataCitations.label.url'),
+ 'description' => '',
+ 'value' => null
+ ]));
+
+ }
+}
\ No newline at end of file
diff --git a/classes/components/forms/publication/PKPDataAvailabilityAndCitationsForm.php b/classes/components/forms/publication/PKPDataAvailabilityAndCitationsForm.php
new file mode 100644
index 00000000000..75eb0ab1c15
--- /dev/null
+++ b/classes/components/forms/publication/PKPDataAvailabilityAndCitationsForm.php
@@ -0,0 +1,49 @@
+action = $action;
+ $this->locales = $locales;
+
+ if ($dataAvailabilitySetting) {
+ $this->addField(new FieldRichTextarea('dataAvailability', [
+ 'label' => __('submission.dataAvailability'),
+ 'tooltip' => __('manager.setup.metadata.dataAvailability.description'),
+ 'isMultilingual' => true,
+ 'value' => $publication->getData('dataAvailability'),
+ 'isRequired' => $isRequired
+ ]));
+ }
+
+ }
+}
diff --git a/classes/components/forms/publication/PKPMetadataForm.php b/classes/components/forms/publication/PKPMetadataForm.php
index c295a85ae0c..1074be1f5c7 100644
--- a/classes/components/forms/publication/PKPMetadataForm.php
+++ b/classes/components/forms/publication/PKPMetadataForm.php
@@ -129,15 +129,6 @@ public function __construct(string $action, array $locales, Publication $publica
]));
}
- if ($this->enabled('dataAvailability')) {
- $this->addField(new FieldRichTextarea('dataAvailability', [
- 'label' => __('submission.dataAvailability'),
- 'tooltip' => __('manager.setup.metadata.dataAvailability.description'),
- 'isMultilingual' => true,
- 'value' => $publication->getData('dataAvailability'),
- ]));
- }
-
if ($this->enabled('pub-id::publisher-id')) {
$this->addField(new FieldText('pub-id::publisher-id', [
'label' => __('submission.publisherId'),
diff --git a/classes/context/Context.php b/classes/context/Context.php
index 5f9d2928820..1ae96efff7d 100644
--- a/classes/context/Context.php
+++ b/classes/context/Context.php
@@ -596,6 +596,7 @@ public function getRequiredMetadata(): array
return collect([
'agencies',
'citations',
+ 'dataCitations',
'coverage',
'dataAvailability',
'disciplines',
diff --git a/classes/core/PKPApplication.php b/classes/core/PKPApplication.php
index 64f310a5fdf..dbbfa932738 100644
--- a/classes/core/PKPApplication.php
+++ b/classes/core/PKPApplication.php
@@ -89,6 +89,7 @@ abstract class PKPApplication implements PKPApplicationInfoProvider
public const ASSOC_TYPE_ACCESSIBLE_FILE_STAGES = 0x010000d;
public const ASSOC_TYPE_NONE = 0x010000e;
public const ASSOC_TYPE_DECISION_TYPE = 0x010000f;
+ public const ASSOC_TYPE_DATA_CITATION = 0x0100010;
// Constant used in UsageStats for submission files that are not full texts
public const ASSOC_TYPE_SUBMISSION_FILE_COUNTER_OTHER = 0x0000213;
@@ -664,6 +665,7 @@ public static function getMetadataFields(): array
'keywords',
'agencies',
'citations',
+ 'dataCitations',
'dataAvailability',
];
}
diff --git a/classes/dataCitation/DataCitation.php b/classes/dataCitation/DataCitation.php
new file mode 100644
index 00000000000..37afc20dc5b
--- /dev/null
+++ b/classes/dataCitation/DataCitation.php
@@ -0,0 +1,97 @@
+settingsTable;
+ }
+
+ /**
+ * @return bool
+ *
+ * @hook DataCitation::add [[$this]]
+ */
+ public function save(array $options = [])
+ {
+
+ $isNew = !$this->exists;
+ $saved = parent::save($options);
+
+ if (!$saved) {
+ return false;
+ }
+
+ // Reload the model to ensure all relationships and settings are loaded
+ $this->refresh();
+
+ if ($isNew) {
+ // This is a new record
+ Hook::call('DataCitation::add', [$this]);
+ } else {
+ // This is an update
+ Hook::call('DataCitation::edit', [$this]);
+ }
+
+ return $saved;
+ }
+
+ /**
+ * Filter by publication ID
+ */
+ protected function scopeWithPublicationId(EloquentBuilder $builder, int $publicationId): EloquentBuilder
+ {
+ return $builder->where('publication_id', $publicationId);
+ }
+
+
+}
diff --git a/classes/dataCitation/Repository.php b/classes/dataCitation/Repository.php
new file mode 100644
index 00000000000..6df3c43809e
--- /dev/null
+++ b/classes/dataCitation/Repository.php
@@ -0,0 +1,91 @@
+ $schemaService */
+ protected PKPSchemaService $schemaService;
+
+ public function __construct(Request $request, PKPSchemaService $schemaService)
+ {
+ $this->request = $request;
+ $this->schemaService = $schemaService;
+ }
+
+ /**
+ * Validate properties for a data citation
+ *
+ * Perform validation checks on data used to add or edit a data citation.
+ *
+ * @param DataCitation|null $dataCitation Data citation being edited. Pass `null` if creating a new citation
+ * @param array $props A key/value array with the new data to validate
+ *
+ * @return array A key/value array with validation errors. Empty if no errors
+ *
+ * @hook DataCitation::validate [[&$errors, $dataCitation, $props]]
+ */
+ public function validate(?DataCitation $dataCitation, array $props): array
+ {
+ $schema = DataCitation::getSchemaName();
+
+ $validator = ValidatorFactory::make(
+ $props,
+ $this->schemaService->getValidationRules($schema, [])
+ );
+
+ // Check required fields
+ ValidatorFactory::required(
+ $validator,
+ $dataCitation,
+ $this->schemaService->getRequiredProps($schema),
+ $this->schemaService->getMultilingualProps($schema),
+ [],
+ ''
+ );
+
+ $errors = [];
+
+ if ($validator->fails()) {
+ $errors = $this->schemaService->formatValidationErrors($validator->errors());
+ }
+
+ Hook::call('DataCitation::validate', [&$errors, $dataCitation, $props]);
+
+ return $errors;
+ }
+
+ /**
+ * Get an instance of the map class for mapping
+ * announcements to their schema
+ */
+ public function getSchemaMap(): maps\Schema
+ {
+ return app('maps')->withExtensions($this->schemaMap);
+ }
+
+}
diff --git a/classes/dataCitation/maps/Schema.php b/classes/dataCitation/maps/Schema.php
new file mode 100644
index 00000000000..378b8b2d7be
--- /dev/null
+++ b/classes/dataCitation/maps/Schema.php
@@ -0,0 +1,113 @@
+mapByProperties($this->getProps(), $item);
+ }
+
+ /**
+ * Summarize a Data Citation
+ *
+ * Includes properties with the apiSummary flag in the Data Citation schema.
+ */
+ public function summarize(DataCitation $item): array
+ {
+ return $this->mapByProperties($this->getSummaryProps(), $item);
+ }
+
+ /**
+ * Map a collection of Data 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 Data 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 Data Citation to an assoc array
+ */
+ protected function mapByProperties(array $props, DataCitation $item): array
+ {
+ $authorModel = $this->getDataCitationAuthorDataModel();
+ $output = [];
+ foreach ($props as $prop) {
+ switch ($prop) {
+ case 'authors':
+ $authors = [];
+ foreach (is_array($item->getAttribute($prop)) ? $item->getAttribute($prop) : [] as $author) {
+ $authors[] = array_merge($authorModel, $author);
+ }
+ $output[$prop] = $authors;
+ break;
+ default:
+ $output[$prop] = $item->getAttribute($prop);
+ break;
+ }
+ }
+ ksort($output);
+ return $this->withExtensions($output, $item);
+ }
+
+ /**
+ * Get author data model as defined in schemas/dataCitation.json.
+ */
+ public function getDataCitationAuthorDataModel(): array
+ {
+ $schemaService = new PKPSchemaService();
+ $schema = $schemaService->get($this->schema);
+ $authorModel = [];
+ foreach (array_keys((array)$schema->properties->authors->items->properties) as $property) {
+ $authorModel[$property] = '';
+ }
+ return $authorModel;
+ }
+
+}
diff --git a/classes/facades/Repo.php b/classes/facades/Repo.php
index e19c89213e8..87978df01dd 100644
--- a/classes/facades/Repo.php
+++ b/classes/facades/Repo.php
@@ -33,6 +33,7 @@
use PKP\category\Repository as CategoryRepository;
use PKP\citation\Repository as CitationRepository;
use PKP\controlledVocab\Repository as ControlledVocabRepository;
+use PKP\dataCitation\Repository as DataCitationRepository;
use PKP\decision\Repository as DecisionRepository;
use PKP\editorialTask\Repository as EditorialTaskRepository;
use PKP\emailTemplate\Repository as EmailTemplateRepository;
@@ -91,6 +92,11 @@ public static function creditRole(): CreditRoleRepository
return app(CreditRoleRepository::class);
}
+ public static function dataCitation(): DataCitationRepository
+ {
+ return app(DataCitationRepository::class);
+ }
+
public static function decision(): DecisionRepository
{
return app()->make(DecisionRepository::class);
diff --git a/classes/migration/install/MetadataMigration.php b/classes/migration/install/MetadataMigration.php
index 773ba2f983c..eba23b0f533 100644
--- a/classes/migration/install/MetadataMigration.php
+++ b/classes/migration/install/MetadataMigration.php
@@ -56,6 +56,34 @@ public function up(): void
$table->unique(['citation_id', 'locale', 'setting_name'], 'citation_settings_unique');
});
+ // Data Citations
+ Schema::create('data_citations', function (Blueprint $table) {
+ $table->comment('A data citation pointing to a related data set.');
+ $table->bigInteger('data_citation_id')->autoIncrement();
+
+ $table->bigInteger('publication_id');
+ $table->foreign('publication_id', 'data_citations_publication')->references('publication_id')->on('publications')->onDelete('cascade');
+ $table->index(['publication_id'], 'data_citations_publication');
+
+ $table->bigInteger('seq')->default(0);
+
+ });
+
+ // Data Citation settings
+ Schema::create('data_citation_settings', function (Blueprint $table) {
+ $table->comment('Additional data about data citations, including localized content.');
+ $table->bigIncrements('data_citation_setting_id');
+ $table->bigInteger('data_citation_id');
+ $table->foreign('data_citation_id', 'data_citation_settings_data_citation_id')->references('data_citation_id')->on('data_citations')->onDelete('cascade');
+ $table->index(['data_citation_id'], 'data_citation_settings_data_citation_id');
+
+ $table->string('locale', 28)->default('');
+ $table->string('setting_name', 255);
+ $table->mediumText('setting_value')->nullable();
+
+ $table->unique(['data_citation_id', 'locale', 'setting_name'], 'data_citation_settings_unique');
+ });
+
// Filter groups
Schema::create('filter_groups', function (Blueprint $table) {
$table->comment('Filter groups are used to organized filters into named sets, which can be retrieved by the application for invocation.');
@@ -120,5 +148,7 @@ public function down(): void
Schema::drop('filter_groups');
Schema::drop('citation_settings');
Schema::drop('citations');
+ Schema::drop('data_citation_settings');
+ Schema::drop('data_citations');
}
}
diff --git a/classes/publication/DAO.php b/classes/publication/DAO.php
index 4503bf29b93..1a5acb0a7c7 100644
--- a/classes/publication/DAO.php
+++ b/classes/publication/DAO.php
@@ -24,6 +24,7 @@
use PKP\controlledVocab\ControlledVocab;
use PKP\core\EntityDAO;
use PKP\core\traits\EntityWithParent;
+use PKP\dataCitation\DataCitation;
use PKP\services\PKPSchemaService;
/**
@@ -175,6 +176,7 @@ function __toString() {
$this->setAuthors($publication);
$this->setCategories($publication);
$this->setControlledVocab($publication);
+ $this->setDataCitations($publication);
return $publication;
}
@@ -237,6 +239,7 @@ public function deleteById(int $publicationId): int
$this->deleteAuthors($publicationId);
$this->deleteCategories($publicationId);
$this->deleteControlledVocab($publicationId);
+ $this->deleteDataCitations($publicationId);
Repo::citation()->deleteByPublicationId($publicationId);
return $affectedRows;
@@ -465,6 +468,23 @@ protected function deleteCategories(int $publicationId): void
PublicationCategory::where('publication_id', $publicationId)->delete();
}
+ /**
+ * Set a publication's Data Citations
+ */
+ protected function setDataCitations(Publication $publication)
+ {
+ $dataCitations = DataCitation::where('publication_id', $publication->getId())->get()->values()->all();
+ $publication->setData('dataCitations', $dataCitations);
+ }
+
+ /**
+ * Delete a publication's Data Citations
+ */
+ protected function deleteDataCitations(int $publicationId)
+ {
+ DataCitation::where('publication_id', $publicationId)->delete();
+ }
+
/**
* Set the DOI object
*
diff --git a/classes/publication/Repository.php b/classes/publication/Repository.php
index 0199f2a8009..d2b7e27ed71 100644
--- a/classes/publication/Repository.php
+++ b/classes/publication/Repository.php
@@ -29,6 +29,7 @@
use PKP\core\Core;
use PKP\core\PKPApplication;
use PKP\core\PKPString;
+use PKP\dataCitation\DataCitation;
use PKP\db\DAORegistry;
use PKP\doi\Doi;
use PKP\facades\Locale;
@@ -398,6 +399,20 @@ public function version(Publication $publication, ?VersionStage $versionStage =
}
}
+ Repo::citation()->importCitations(
+ $newPublication->getId(),
+ $newPublication->getData('citationsRaw')
+ );
+
+ // Clone data citations if any
+ $dataCitations = DataCitation::where('publication_id', $publication->getId())->get();
+ foreach ($dataCitations as $dataCitation) {
+ $data = $dataCitation->toArray();
+ unset($data['dataCitationId']);
+ $data['publicationId'] = $newPublication->getId();
+ $newDataCitation = DataCitation::create($data);
+ }
+
$genreDao = DAORegistry::getDAO('GenreDAO'); /** @var \PKP\submission\GenreDAO $genreDao */
$genres = $genreDao->getEnabledByContextId($context->getId());
diff --git a/classes/publication/maps/Schema.php b/classes/publication/maps/Schema.php
index 6b5b62ba5b9..315b04297bc 100644
--- a/classes/publication/maps/Schema.php
+++ b/classes/publication/maps/Schema.php
@@ -20,6 +20,7 @@
use APP\submission\Submission;
use Illuminate\Support\Enumerable;
use PKP\context\Context;
+use PKP\dataCitation\DataCitation;
use PKP\services\PKPSchemaService;
use PKP\submission\Genre;
@@ -141,6 +142,13 @@ protected function mapByProperties(array $props, Publication $publication, bool
case 'citationsRaw':
$output[$prop] = Repo::citation()->getRawCitationsByPublicationId($publication->getId())->implode(PHP_EOL);
break;
+ case 'dataCitations':
+ $data = [];
+ foreach ($publication->getData('dataCitations') as $dataCitation) {
+ $data[] = Repo::dataCitation()->getSchemaMap()->map($dataCitation);
+ }
+ $output[$prop] = $data;
+ break;
case 'doiObject':
if ($publication->getData('doiObject')) {
$retVal = Repo::doi()->getSchemaMap()->summarize($publication->getData('doiObject'));
diff --git a/classes/security/authorization/internal/DataCitationRequiredPolicy.php b/classes/security/authorization/internal/DataCitationRequiredPolicy.php
new file mode 100644
index 00000000000..c57c633a2ce
--- /dev/null
+++ b/classes/security/authorization/internal/DataCitationRequiredPolicy.php
@@ -0,0 +1,82 @@
+getDataObjectId();
+ if (!$dataCitationId) {
+ return AuthorizationPolicy::AUTHORIZATION_DENY;
+ }
+
+ // Need a valid submission in request.
+ $submission = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION);
+ if (!$submission instanceof Submission) {
+ return AuthorizationPolicy::AUTHORIZATION_DENY;
+ }
+
+ // Need a valid publication in request
+ $publication = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_PUBLICATION);
+ if (!$publication instanceof Publication) {
+ return AuthorizationPolicy::AUTHORIZATION_DENY;
+ }
+
+ // Make sure the dataCitation belongs to the submission.
+ $dataCitation = DataCitation::where('data_citation_id', $dataCitationId)
+ ->where('publication_id', $publication->getId())
+ ->first();
+ if (!$dataCitation instanceof DataCitation) {
+ return AuthorizationPolicy::AUTHORIZATION_DENY;
+ }
+
+ // Save the dataCitation to the authorization context.
+ $this->addAuthorizedContextObject(Application::ASSOC_TYPE_DATA_CITATION, $dataCitation);
+ return AuthorizationPolicy::AUTHORIZATION_PERMIT;
+ }
+}
+
+if (!PKP_STRICT_MODE) {
+ class_alias('\PKP\security\authorization\internal\DataCitationRequiredPolicy', '\DataCitationRequiredPolicy');
+}
diff --git a/classes/services/PKPSchemaService.php b/classes/services/PKPSchemaService.php
index 21f1f60e94f..fcb5a0a7474 100644
--- a/classes/services/PKPSchemaService.php
+++ b/classes/services/PKPSchemaService.php
@@ -36,6 +36,7 @@ class PKPSchemaService
public const SCHEMA_CITATION = 'citation';
public const SCHEMA_CONTEXT = 'context';
public const SCHEMA_CONTRIBUTOR_ROLE = 'contributorRole';
+ public const SCHEMA_DATA_CITATION = 'dataCitation';
public const SCHEMA_DOI = 'doi';
public const SCHEMA_DECISION = 'decision';
public const SCHEMA_EMAIL_TEMPLATE = 'emailTemplate';
@@ -377,6 +378,9 @@ public function coerce($value, $type, $schema)
foreach ($value as $i => $v) {
$newArray[$i] = $this->coerce($v, $schema->items->type, $schema->items);
}
+ } elseif (is_string($value) && json_decode($value) !== null && json_last_error() === JSON_ERROR_NONE) {
+ // If the value is already a JSON string, do not attempt to serialize it again
+ return $value;
} else {
$newArray[] = serialize($value);
}
@@ -394,6 +398,7 @@ public function coerce($value, $type, $schema)
}
$newObject[$propName] = $this->coerce($value[$propName], $propSchema->type, $propSchema);
}
+
return $newObject;
}
throw new Exception('Requested variable coercion for a type that was not recognized: ' . $type);
diff --git a/classes/submission/Repository.php b/classes/submission/Repository.php
index e1e7dda56c4..886fda26aff 100644
--- a/classes/submission/Repository.php
+++ b/classes/submission/Repository.php
@@ -432,6 +432,7 @@ public function validateSubmit(Submission $submission, Context $context): array
if ($metadata === 'citations') {
$metadata = 'citationsRaw';
}
+
// The `supportingAgencies` metadata is called `agencies` on the context
if ($metadata === 'agencies') {
$metadata = 'supportingAgencies';
diff --git a/locale/en/common.po b/locale/en/common.po
index 585c370d6d1..659e67333c9 100644
--- a/locale/en/common.po
+++ b/locale/en/common.po
@@ -1839,6 +1839,9 @@ msgstr "Title"
msgid "search.typeMethodApproach"
msgstr "Type (method/approach)"
+msgid "grid.action.addDataCitation"
+msgstr "Add a new Data Citation"
+
msgid "grid.action.downloadFile"
msgstr "Download this file"
diff --git a/locale/en/manager.po b/locale/en/manager.po
index a6c20a4f276..116b91402c4 100644
--- a/locale/en/manager.po
+++ b/locale/en/manager.po
@@ -2209,6 +2209,25 @@ msgstr ""
"Require the author to suggest coverage metadata before accepting their "
"submission."
+msgid "manager.setup.metadata.dataCitations"
+msgstr "Data Citations"
+
+msgid "manager.setup.metadata.dataCitations.description"
+msgstr "A data citation typically identifies a dataset that supports the findings of the work. Data citations ensure reproducibility, transparency, and proper attribution of research data."
+
+msgid "manager.setup.metadata.dataCitations.enable"
+msgstr "Enable data citation metadata"
+
+msgid "manager.setup.metadata.dataCitations.noRequest"
+msgstr "Do not request data citation metadata from the author during submission."
+
+msgid "manager.setup.metadata.dataCitations.request"
+msgstr "Ask the author for data citation metadata during submission."
+
+msgid "manager.setup.metadata.dataCitations.require"
+msgstr ""
+"Require the author to add data citation metadata before accepting their submission."
+
msgid "manager.setup.metadata.keywords.description"
msgstr ""
"Keywords are typically one- to three-word phrases that are used to indicate "
diff --git a/locale/en/submission.po b/locale/en/submission.po
index 83245271739..0ba3fedd37e 100644
--- a/locale/en/submission.po
+++ b/locale/en/submission.po
@@ -692,6 +692,63 @@ msgstr "The following references have been extracted and will be linked to the s
msgid "submission.parsedAndSaveCitations"
msgstr "Extract and Save References"
+msgid "submission.dataCitations"
+msgstr "Data Citations"
+
+msgid "submission.dataCitations.description"
+msgstr "This table allows users to add formal data citations, ensuring datasets are properly credited and appear alongside other references in the publication."
+
+msgid "submission.dataCitations.title"
+msgstr "Title"
+
+msgid "submission.dataCitations.addModal.title"
+msgstr "Add Data Citation"
+
+msgid "submission.dataCitations.editModal.title"
+msgstr "Edit Data Citation"
+
+msgid "submission.dataCitations.label.title"
+msgstr "Title"
+
+msgid "submission.dataCitations.label.identifierType"
+msgstr "Identifier type"
+
+msgid "submission.dataCitations.label.identifier"
+msgstr "Identifier"
+
+msgid "submission.dataCitations.label.relationshipType"
+msgstr "Relationship type"
+
+msgid "submission.dataCitations.label.relationshipType.supporting"
+msgstr "Supporting data without specifying whether they were generated or analyzed (supporting)."
+
+msgid "submission.dataCitations.label.relationshipType.generated"
+msgstr "Supporting data that were generated for the study (generated)."
+
+msgid "submission.dataCitations.label.relationshipType.analyzed"
+msgstr "Supporting data that were analyzed but not generated for the study (analyzed)."
+
+msgid "submission.dataCitations.label.relationshipType.non-analyzed"
+msgstr "Referenced data that were neither generated nor analyzed for the study (non-analyzed)."
+
+msgid "submission.dataCitations.label.repository"
+msgstr "Repository"
+
+msgid "submission.dataCitations.label.year"
+msgstr "Year"
+
+msgid "submission.dataCitations.label.creators"
+msgstr "Creators"
+
+msgid "submission.dataCitations.label.url"
+msgstr "URL"
+
+msgid "submission.dataCitations.emptyCitations"
+msgstr "No data citations have been added."
+
+msgid "submission.dataCitations.required"
+msgstr "Data citations are required."
+
msgid "submission.comments.addComment"
msgstr "Add Comment"
@@ -749,6 +806,9 @@ msgstr "Discussion Files"
msgid "submission.coverage"
msgstr "Coverage Information"
+msgid "submission.dataAvailabilityAndCitation.data"
+msgstr "Data"
+
msgid "submission.details"
msgstr "Submission Details"
diff --git a/pages/authorDashboard/PKPAuthorDashboardHandler.php b/pages/authorDashboard/PKPAuthorDashboardHandler.php
index 4c4cf89af4d..509f05166b1 100644
--- a/pages/authorDashboard/PKPAuthorDashboardHandler.php
+++ b/pages/authorDashboard/PKPAuthorDashboardHandler.php
@@ -25,6 +25,7 @@
use APP\submission\Submission;
use APP\template\TemplateManager;
use Illuminate\Support\Enumerable;
+use PKP\components\forms\publication\PKPDataAvailabilityAndCitationsForm;
use PKP\components\forms\publication\PKPMetadataForm;
use PKP\components\forms\publication\TitleAbstractForm;
use PKP\components\listPanels\ContributorsListPanel;
@@ -240,9 +241,11 @@ public function setupTemplate($request)
);
$titleAbstractForm = $this->getTitleAbstractForm($latestPublicationApiUrl, $locales, $latestPublication, $submissionContext);
+ $dataAvailabilityAndCitationsForm = new PKPDataAvailabilityAndCitationsForm($latestPublicationApiUrl, $locales, $latestPublication);
$templateMgr->setConstants([
'FORM_TITLE_ABSTRACT' => $titleAbstractForm::FORM_TITLE_ABSTRACT,
+ 'FORM_DATA_AVAILABILITY_AND_CITATIONS' => $dataAvailabilityAndCitationsForm ::FORM_DATA_AVAILABILITY_AND_CITATIONS,
]);
// Get the submission props without the full publication details. We'll
@@ -301,12 +304,14 @@ public function setupTemplate($request)
'components' => [
$titleAbstractForm::FORM_TITLE_ABSTRACT => $this->getLocalizedForm($titleAbstractForm, $submissionLocale, $locales),
$citationsForm::FORM_CITATIONS => $this->getLocalizedForm($citationsForm, $submissionLocale, $locales),
+ $dataAvailabilityAndCitationsForm::FORM_DATA_AVAILABILITY_AND_CITATIONS => $this->getLocalizedForm($dataAvailabilityAndCitationsForm, $submissionLocale, $locales),
$contributorsListPanel->id => $contributorsListPanel->getConfig(),
],
'currentPublication' => $currentPublicationProps,
'publicationFormIds' => [
$titleAbstractForm::FORM_TITLE_ABSTRACT,
$citationsForm::FORM_CITATIONS,
+ $dataAvailabilityAndCitationsForm::FORM_DATA_AVAILABILITY_AND_CITATIONS,
],
'representationsGridUrl' => $canAccessProductionStage ? $this->_getRepresentationsGridUrl($request, $submission) : '',
'submission' => $submissionProps,
@@ -316,6 +321,8 @@ public function setupTemplate($request)
'submissionLibraryLabel' => __('grid.libraryFiles.submission.title'),
'submissionLibraryUrl' => $submissionLibraryUrl,
'supportsReferences' => !!$submissionContext->getData('citations'),
+ 'supportsDataAvailability' => !!$submissionContext->getData('dataAvailability'),
+ 'supportsDataCitations' => !!$submissionContext->getData('dataCitations'),
'statusLabel' => __('semicolon', ['label' => __('common.status')]),
'uploadFileModalLabel' => __('editor.submissionReview.uploadFile'),
'uploadFileUrl' => $uploadFileUrl,
diff --git a/pages/dashboard/PKPDashboardHandler.php b/pages/dashboard/PKPDashboardHandler.php
index f46ac6bd779..ef3e9740d13 100644
--- a/pages/dashboard/PKPDashboardHandler.php
+++ b/pages/dashboard/PKPDashboardHandler.php
@@ -27,6 +27,7 @@
use PKP\components\forms\citation\CitationStructuredEditForm;
use PKP\components\forms\decision\LogReviewerResponseForm;
use PKP\components\forms\publication\ContributorForm;
+use PKP\components\forms\dataCitation\DataCitationEditForm;
use PKP\controllers\grid\users\reviewer\PKPReviewerGridHandler;
use PKP\core\JSONMessage;
use PKP\core\PKPApplication;
@@ -174,6 +175,8 @@ public function index($args, $request)
$logResponseForm = new LogReviewerResponseForm($context->getSupportedFormLocales(), $context);
$citationStructuredEditForm = new CitationStructuredEditForm('emit');
$citationRawEditForm = new CitationRawEditForm('emit');
+ $dataCitationEditForm = new DataCitationEditForm('emit');
+
$templateMgr->setState([
'pageInitConfig' => [
'selectRevisionDecisionForm' => $selectRevisionDecisionForm->getConfig(),
@@ -186,6 +189,8 @@ public function index($args, $request)
'contextCitationsMetadataLookup' => $context->getData('citationsMetadataLookup') ?: 0,
'publicationSettings' => [
'supportsCitations' => !!$context->getData('citations'),
+ 'supportsDataCitations' => !!$context->getData('dataCitations'),
+ 'supportsDataAvailability' => !!$context->getData('dataAvailability'),
'identifiersEnabled' => $identifiersEnabled,
'isReviewerSuggestionEnabled' => (bool)$context->getData('reviewerSuggestionEnabled'),
],
@@ -194,7 +199,8 @@ public function index($args, $request)
'logResponseForm' => $logResponseForm->getConfig(),
'versionStageOptions' => $versionStageOptions,
'citationStructuredEditForm' => $citationStructuredEditForm->getConfig(),
- 'citationRawEditForm' => $citationRawEditForm->getConfig()
+ 'citationRawEditForm' => $citationRawEditForm->getConfig(),
+ 'dataCitationEditForm' => $dataCitationEditForm->getConfig()
],
]
]);
diff --git a/pages/submission/PKPSubmissionHandler.php b/pages/submission/PKPSubmissionHandler.php
index 04145d9b5ae..7dfcdbd7cdf 100644
--- a/pages/submission/PKPSubmissionHandler.php
+++ b/pages/submission/PKPSubmissionHandler.php
@@ -29,6 +29,8 @@
use Illuminate\Support\LazyCollection;
use PKP\components\forms\FormComponent;
use PKP\components\forms\publication\PKPCitationsForm;
+use PKP\components\forms\dataCitation\DataCitationEditForm;
+use PKP\components\forms\publication\PKPDataAvailabilityAndCitationsForm;
use PKP\components\forms\publication\TitleAbstractForm;
use PKP\components\forms\submission\CommentsForTheEditors;
use PKP\components\forms\submission\ConfirmSubmission;
@@ -52,6 +54,7 @@ abstract class PKPSubmissionHandler extends Handler
{
public const SECTION_TYPE_CONFIRM = 'confirm';
public const SECTION_TYPE_CONTRIBUTORS = 'contributors';
+ public const SECTION_TYPE_DATA_CITATIONS = 'dataCitations';
public const SECTION_TYPE_REVIEWER_SUGGESTIONS = 'reviewerSuggestions';
public const SECTION_TYPE_FILES = 'files';
public const SECTION_TYPE_FORM = 'form';
@@ -232,6 +235,14 @@ protected function showWizard(array $args, Request $request, Submission $submiss
$components[$reviewerSuggestionsListPanel->id] = $reviewerSuggestionsListPanel->getConfig();
}
+ $dataCitationsSetting = $context->getData('dataCitations');
+ if (in_array($dataCitationsSetting, [Context::METADATA_REQUEST, Context::METADATA_REQUIRE])) {
+ $dataCitationEditForm = new DataCitationEditForm('emit');
+ $components['dataCitation'] = [
+ 'dataCitationEditForm' => $dataCitationEditForm->getConfig(),
+ ];
+ }
+
$userRoles = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_USER_ROLES);
$templateMgr = TemplateManager::getManager($request);
@@ -760,6 +771,40 @@ protected function getDetailsStep(
];
}
+
+ $dataAvailabilitySetting = $request->getContext()->getData('dataAvailability');
+ if (in_array($dataAvailabilitySetting, [Context::METADATA_REQUEST, Context::METADATA_REQUIRE])) {
+
+ $dataAvailabilityAndCitationsForm = new PKPDataAvailabilityAndCitationsForm(
+ $publicationApiUrl,
+ $locales,
+ $publication,
+ in_array($dataAvailabilitySetting, [Context::METADATA_REQUEST, Context::METADATA_REQUIRE]),
+ $dataAvailabilitySetting === Context::METADATA_REQUIRE
+ );
+
+ $this->removeButtonFromForm($dataAvailabilityAndCitationsForm);
+
+ $sections[] = [
+ 'id' => $dataAvailabilityAndCitationsForm->id,
+ 'name' => 'Data Availability',
+ 'type' => self::SECTION_TYPE_FORM,
+ 'description' => '',
+ 'form' => $dataAvailabilityAndCitationsForm->getConfig(),
+ ];
+
+ }
+
+ $dataCitationsSetting = $request->getContext()->getData('dataCitations');
+ if (in_array($dataCitationsSetting, [Context::METADATA_REQUEST, Context::METADATA_REQUIRE])) {
+ $sections[] = [
+ 'id' => 'dataCitations',
+ 'name' => __('submission.dataCitations'),
+ 'type' => self::SECTION_TYPE_DATA_CITATIONS,
+ 'description' => __('submission.dataCitations.description'),
+ ];
+ }
+
return [
'id' => 'details',
'name' => __('common.details'),
@@ -767,7 +812,10 @@ protected function getDetailsStep(
'sections' => $sections,
'reviewTemplate' => '/submission/review-details.tpl',
];
+
+
}
+
/**
* Get the state for the For the Editors step
diff --git a/plugins/importexport/native/filter/NativeXmlPKPPublicationFilter.php b/plugins/importexport/native/filter/NativeXmlPKPPublicationFilter.php
index 5bdbd839935..2cd5c5ca9be 100644
--- a/plugins/importexport/native/filter/NativeXmlPKPPublicationFilter.php
+++ b/plugins/importexport/native/filter/NativeXmlPKPPublicationFilter.php
@@ -192,6 +192,7 @@ public function handleChildElement($n, $publication)
case 'authors':
$this->parseAuthors($n, $publication);
break;
+// DATACITATIONS TODO
case 'citations':
$this->parseCitations($n, $publication);
break;
@@ -290,6 +291,8 @@ public function parseAuthor($n, $publication)
return $this->importWithXMLNode($n, 'native-xml=>author');
}
+// DATACITATIONS TODO
+
/**
* Parse a publication citation and add it to the publication.
*
diff --git a/plugins/importexport/native/filter/PKPPublicationNativeXmlFilter.php b/plugins/importexport/native/filter/PKPPublicationNativeXmlFilter.php
index 2fea5cce359..bb8ba8943a0 100644
--- a/plugins/importexport/native/filter/PKPPublicationNativeXmlFilter.php
+++ b/plugins/importexport/native/filter/PKPPublicationNativeXmlFilter.php
@@ -134,6 +134,8 @@ public function createEntityNode($doc, $entity)
$this->addRepresentations($doc, $entityNode, $entity);
+// DATACITATIONS TODO
+
$citationsListNode = $this->createCitationsNode($doc, $deployment, $entity);
if ($citationsListNode->hasChildNodes() || $citationsListNode->hasAttributes()) {
$entityNode->appendChild($citationsListNode);
@@ -342,6 +344,8 @@ public function getFiles($representation)
assert(false); // To be overridden by subclasses
}
+// DATACITATIONS TODO
+
/**
* Create and return a Citations node.
*
diff --git a/plugins/importexport/native/pkp-native.xsd b/plugins/importexport/native/pkp-native.xsd
index 5baa28a9e02..e76d5282674 100644
--- a/plugins/importexport/native/pkp-native.xsd
+++ b/plugins/importexport/native/pkp-native.xsd
@@ -290,6 +290,7 @@