Browse Source

added DOI field and DOI lookup

main
Paul Pound 1 week ago
parent
commit
1f10ed952f
  1. 9
      config/install/core.entity_form_display.node.ill_institution_request.default.yml
  2. 8
      config/install/core.entity_view_display.node.ill_institution_request.default.yml
  3. 21
      config/install/field.field.node.ill_institution_request.field_ill_doi.yml
  4. 23
      config/install/field.storage.node.field_ill_doi.yml
  5. 102
      ill_corporate_forms.install
  6. 337
      ill_corporate_forms.module

9
config/install/core.entity_form_display.node.ill_institution_request.default.yml

@ -9,6 +9,7 @@ dependencies:
- field.field.node.ill_institution_request.field_ill_chapter_author - field.field.node.ill_institution_request.field_ill_chapter_author
- field.field.node.ill_institution_request.field_ill_chapter_title - field.field.node.ill_institution_request.field_ill_chapter_title
- field.field.node.ill_institution_request.field_ill_copyright_agreement - field.field.node.ill_institution_request.field_ill_copyright_agreement
- field.field.node.ill_institution_request.field_ill_doi
- field.field.node.ill_institution_request.field_ill_edition - field.field.node.ill_institution_request.field_ill_edition
- field.field.node.ill_institution_request.field_ill_institution_code - field.field.node.ill_institution_request.field_ill_institution_code
- field.field.node.ill_institution_request.field_ill_isbn - field.field.node.ill_institution_request.field_ill_isbn
@ -58,6 +59,14 @@ content:
region: content region: content
settings: {} settings: {}
third_party_settings: {} third_party_settings: {}
field_ill_doi:
type: string_textfield
weight: 5
region: content
settings:
size: 60
placeholder: ''
third_party_settings: {}
field_ill_req_email: field_ill_req_email:
type: email_default type: email_default
weight: 12 weight: 12

8
config/install/core.entity_view_display.node.ill_institution_request.default.yml

@ -9,6 +9,7 @@ dependencies:
- field.field.node.ill_institution_request.field_ill_chapter_author - field.field.node.ill_institution_request.field_ill_chapter_author
- field.field.node.ill_institution_request.field_ill_chapter_title - field.field.node.ill_institution_request.field_ill_chapter_title
- field.field.node.ill_institution_request.field_ill_copyright_agreement - field.field.node.ill_institution_request.field_ill_copyright_agreement
- field.field.node.ill_institution_request.field_ill_doi
- field.field.node.ill_institution_request.field_ill_edition - field.field.node.ill_institution_request.field_ill_edition
- field.field.node.ill_institution_request.field_ill_institution_code - field.field.node.ill_institution_request.field_ill_institution_code
- field.field.node.ill_institution_request.field_ill_isbn - field.field.node.ill_institution_request.field_ill_isbn
@ -59,6 +60,13 @@ content:
label: above label: above
settings: {} settings: {}
third_party_settings: {} third_party_settings: {}
field_ill_doi:
type: string
weight: 5
region: content
label: above
settings: {}
third_party_settings: {}
field_ill_req_email: field_ill_req_email:
type: email_mailto type: email_mailto
weight: 12 weight: 12

21
config/install/field.field.node.ill_institution_request.field_ill_doi.yml

@ -0,0 +1,21 @@
langcode: en
status: true
dependencies:
config:
- field.storage.node.field_ill_doi
- node.type.ill_institution_request
enforced:
module:
- ill_corporate_forms
id: node.ill_institution_request.field_ill_doi
field_name: field_ill_doi
entity_type: node
bundle: ill_institution_request
label: 'DOI'
description: 'Digital Object Identifier (e.g., 10.1016/j.ypmed.2019.01.018)'
required: false
translatable: false
default_value: {}
default_value_callback: ''
settings: {}
field_type: string

23
config/install/field.storage.node.field_ill_doi.yml

@ -0,0 +1,23 @@
langcode: en
status: true
dependencies:
module:
- node
enforced:
module:
- ill_corporate_forms
id: node.field_ill_doi
field_name: field_ill_doi
entity_type: node
type: string
settings:
max_length: 255
is_ascii: false
case_sensitive: false
module: core
locked: false
cardinality: 1
translatable: true
indexes: {}
persist_with_no_fields: false
custom_storage: false

102
ill_corporate_forms.install

@ -5,6 +5,108 @@
* Install, update, and uninstall functions for the ILL Corporate Forms module. * Install, update, and uninstall functions for the ILL Corporate Forms module.
*/ */
/**
* Add DOI field to the ill_institution_request content type.
*/
function ill_corporate_forms_update_10001(): void {
$entity_type_manager = \Drupal::entityTypeManager();
// Create field storage if it doesn't already exist.
$field_storage_config = $entity_type_manager->getStorage('field_storage_config');
if (!$field_storage_config->load('node.field_ill_doi')) {
$field_storage_config->create([
'langcode' => 'en',
'status' => TRUE,
'dependencies' => [
'module' => ['node'],
'enforced' => [
'module' => ['ill_corporate_forms'],
],
],
'id' => 'node.field_ill_doi',
'field_name' => 'field_ill_doi',
'entity_type' => 'node',
'type' => 'string',
'settings' => [
'max_length' => 255,
'is_ascii' => FALSE,
'case_sensitive' => FALSE,
],
'module' => 'core',
'locked' => FALSE,
'cardinality' => 1,
'translatable' => TRUE,
'indexes' => [],
'persist_with_no_fields' => FALSE,
'custom_storage' => FALSE,
])->save();
}
// Create field instance if it doesn't already exist.
$field_config = $entity_type_manager->getStorage('field_config');
if (!$field_config->load('node.ill_institution_request.field_ill_doi')) {
$field_config->create([
'langcode' => 'en',
'status' => TRUE,
'dependencies' => [
'config' => [
'field.storage.node.field_ill_doi',
'node.type.ill_institution_request',
],
'enforced' => [
'module' => ['ill_corporate_forms'],
],
],
'id' => 'node.ill_institution_request.field_ill_doi',
'field_name' => 'field_ill_doi',
'entity_type' => 'node',
'bundle' => 'ill_institution_request',
'label' => 'DOI',
'description' => 'Digital Object Identifier (e.g., 10.1016/j.ypmed.2019.01.018)',
'required' => FALSE,
'translatable' => FALSE,
'default_value' => [],
'default_value_callback' => '',
'settings' => [],
'field_type' => 'string',
])->save();
}
// Add the field to the form display.
/** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $form_display */
$form_display = $entity_type_manager
->getStorage('entity_form_display')
->load('node.ill_institution_request.default');
if ($form_display && !$form_display->getComponent('field_ill_doi')) {
$form_display->setComponent('field_ill_doi', [
'type' => 'string_textfield',
'weight' => 5,
'region' => 'content',
'settings' => [
'size' => 60,
'placeholder' => '',
],
'third_party_settings' => [],
])->save();
}
// Add the field to the view display.
/** @var \Drupal\Core\Entity\Display\EntityViewDisplayInterface $view_display */
$view_display = $entity_type_manager
->getStorage('entity_view_display')
->load('node.ill_institution_request.default');
if ($view_display && !$view_display->getComponent('field_ill_doi')) {
$view_display->setComponent('field_ill_doi', [
'type' => 'string',
'weight' => 5,
'region' => 'content',
'label' => 'above',
'settings' => [],
'third_party_settings' => [],
])->save();
}
}
/** /**
* Implements hook_uninstall(). * Implements hook_uninstall().
*/ */

337
ill_corporate_forms.module

@ -6,12 +6,14 @@
*/ */
use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Form\FormStateInterface;
use GuzzleHttp\Exception\GuzzleException;
/** /**
* Implements hook_form_FORM_ID_alter() for the ill_institution_request node form. * Implements hook_form_FORM_ID_alter() for the ill_institution_request node form.
*/ */
function ill_corporate_forms_form_node_ill_institution_request_form_alter(array &$form, FormStateInterface $form_state, string $form_id): void { function ill_corporate_forms_form_node_ill_institution_request_form_alter(array &$form, FormStateInterface $form_state, string $form_id): void {
_ill_corporate_forms_add_item_type_states($form); _ill_corporate_forms_add_item_type_states($form);
_ill_corporate_forms_add_doi_lookup($form, $form_state);
$form['#validate'][] = '_ill_corporate_forms_validate_partner_email'; $form['#validate'][] = '_ill_corporate_forms_validate_partner_email';
$form['#validate'][] = '_ill_corporate_forms_validate_institution_code'; $form['#validate'][] = '_ill_corporate_forms_validate_institution_code';
$form['#validate'][] = '_ill_corporate_forms_validate_request_emails'; $form['#validate'][] = '_ill_corporate_forms_validate_request_emails';
@ -25,6 +27,7 @@ function ill_corporate_forms_form_node_ill_institution_request_form_alter(array
*/ */
function ill_corporate_forms_form_node_ill_institution_request_edit_form_alter(array &$form, FormStateInterface $form_state, string $form_id): void { function ill_corporate_forms_form_node_ill_institution_request_edit_form_alter(array &$form, FormStateInterface $form_state, string $form_id): void {
_ill_corporate_forms_add_item_type_states($form); _ill_corporate_forms_add_item_type_states($form);
_ill_corporate_forms_add_doi_lookup($form, $form_state);
$form['#validate'][] = '_ill_corporate_forms_validate_partner_email'; $form['#validate'][] = '_ill_corporate_forms_validate_partner_email';
$form['#validate'][] = '_ill_corporate_forms_validate_institution_code'; $form['#validate'][] = '_ill_corporate_forms_validate_institution_code';
$form['#validate'][] = '_ill_corporate_forms_validate_request_emails'; $form['#validate'][] = '_ill_corporate_forms_validate_request_emails';
@ -63,6 +66,340 @@ function ill_corporate_forms_form_node_ill_request_tracking_edit_form_alter(arra
$form['title']['#access'] = FALSE; $form['title']['#access'] = FALSE;
} }
/**
* Adds the DOI lookup button and AJAX handling to the request form.
*
* @param array $form
* The form render array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*/
function _ill_corporate_forms_add_doi_lookup(array &$form, FormStateInterface $form_state): void {
// Wrap the form so AJAX can replace it.
$form['#prefix'] = '<div id="ill-request-form-wrapper">';
$form['#suffix'] = '</div>';
// Add the Lookup DOI button right after the DOI field.
if (isset($form['field_ill_doi'])) {
$form['doi_lookup_button'] = [
'#type' => 'submit',
'#value' => t('Lookup DOI'),
'#weight' => 6,
'#limit_validation_errors' => [['field_ill_doi']],
'#submit' => ['_ill_corporate_forms_doi_lookup_submit'],
'#ajax' => [
'callback' => '_ill_corporate_forms_doi_lookup_callback',
'wrapper' => 'ill-request-form-wrapper',
'effect' => 'fade',
],
'#attributes' => ['class' => ['button--secondary']],
];
}
}
/**
* Submit handler for the Lookup DOI button.
*
* Fetches CrossRef data and stores it in form state for rebuilding.
*/
function _ill_corporate_forms_doi_lookup_submit(array &$form, FormStateInterface $form_state): void {
$doi_value = $form_state->getValue('field_ill_doi');
$doi = _ill_corporate_forms_extract_field_value($doi_value);
if (empty($doi)) {
\Drupal::messenger()->addWarning(t('Please enter a DOI before clicking Lookup DOI.'));
$form_state->setRebuild(TRUE);
return;
}
// Normalize the DOI (strip URL prefix, "doi:" prefix, etc.).
$doi = _ill_corporate_forms_normalize_doi($doi);
// Fetch data from CrossRef.
$data = _ill_corporate_forms_crossref_fetch($doi);
if ($data === NULL) {
\Drupal::messenger()->addWarning(t('Could not retrieve data for DOI "@doi". Please verify the DOI and try again.', ['@doi' => $doi]));
}
else {
// Inject the fetched data directly into user input so the form widgets
// pick up the values on rebuild. Setting #default_value does not work
// because Drupal's Form API uses cached user input over defaults.
_ill_corporate_forms_inject_doi_values($form_state, $data);
\Drupal::messenger()->addStatus(t('DOI lookup successful. Fields have been populated.'));
}
$form_state->setRebuild(TRUE);
}
/**
* AJAX callback for the DOI lookup button.
*
* Returns the rebuilt form.
*/
function _ill_corporate_forms_doi_lookup_callback(array &$form, FormStateInterface $form_state) {
return $form;
}
/**
* Normalizes a DOI string by removing common prefixes.
*
* Handles formats like:
* - https://doi.org/10.1234/example
* - http://dx.doi.org/10.1234/example
* - doi:10.1234/example
* - 10.1234/example
*
* @param string $doi
* The raw DOI input.
*
* @return string
* The normalized DOI (e.g., "10.1234/example").
*/
function _ill_corporate_forms_normalize_doi(string $doi): string {
$doi = trim($doi);
// Remove URL prefixes.
$doi = preg_replace('#^https?://(dx\.)?doi\.org/#i', '', $doi);
// Remove "doi:" prefix.
$doi = preg_replace('/^doi:\s*/i', '', $doi);
return $doi;
}
/**
* Fetches bibliographic data from the CrossRef REST API.
*
* @param string $doi
* A normalized DOI string.
*
* @return array|null
* An associative array of bibliographic data, or NULL on failure.
*/
function _ill_corporate_forms_crossref_fetch(string $doi): ?array {
$url = 'https://api.crossref.org/works/' . urlencode($doi);
try {
/** @var \GuzzleHttp\ClientInterface $client */
$client = \Drupal::httpClient();
$response = $client->request('GET', $url, [
'headers' => [
'Accept' => 'application/json',
'User-Agent' => 'ILLCorporateForms/1.0 (Drupal module; mailto:ill@library.example.com)',
],
'timeout' => 10,
]);
if ($response->getStatusCode() !== 200) {
return NULL;
}
$body = json_decode((string) $response->getBody(), TRUE);
if (empty($body['message'])) {
return NULL;
}
return _ill_corporate_forms_parse_crossref($body['message']);
}
catch (GuzzleException $e) {
\Drupal::logger('ill_corporate_forms')->warning('CrossRef API request failed for DOI @doi: @message', [
'@doi' => $doi,
'@message' => $e->getMessage(),
]);
return NULL;
}
}
/**
* Parses a CrossRef API message into a normalized data array.
*
* @param array $message
* The "message" portion of a CrossRef API response.
*
* @return array
* Associative array with keys matching our field names.
*/
function _ill_corporate_forms_parse_crossref(array $message): array {
$data = [];
// Determine the item type.
$type = $message['type'] ?? '';
switch ($type) {
case 'journal-article':
$data['item_type'] = 'article';
break;
case 'book':
case 'monograph':
case 'edited-book':
case 'reference-book':
$data['item_type'] = 'book';
break;
case 'book-chapter':
case 'book-section':
case 'book-part':
$data['item_type'] = 'book_chapter';
break;
default:
// Default to article for journal-related types, book otherwise.
if (str_contains($type, 'journal') || str_contains($type, 'article')) {
$data['item_type'] = 'article';
}
}
// Title — maps differently based on type.
$title = $message['title'][0] ?? '';
$container_title = $message['container-title'][0] ?? '';
if (($data['item_type'] ?? '') === 'article') {
$data['article_title'] = $title;
$data['journal_title'] = $container_title;
}
elseif (($data['item_type'] ?? '') === 'book_chapter') {
$data['chapter_title'] = $title;
$data['item_title'] = $container_title;
}
else {
$data['item_title'] = $title;
}
// Authors.
$authors = [];
$editors = [];
foreach ($message['author'] ?? [] as $person) {
$name_parts = [];
if (!empty($person['given'])) {
$name_parts[] = $person['given'];
}
if (!empty($person['family'])) {
$name_parts[] = $person['family'];
}
if (!empty($name_parts)) {
$authors[] = implode(' ', $name_parts);
}
}
foreach ($message['editor'] ?? [] as $person) {
$name_parts = [];
if (!empty($person['given'])) {
$name_parts[] = $person['given'];
}
if (!empty($person['family'])) {
$name_parts[] = $person['family'];
}
if (!empty($name_parts)) {
$editors[] = implode(' ', $name_parts);
}
}
$author_string = implode('; ', $authors);
if (($data['item_type'] ?? '') === 'article') {
$data['article_author'] = $author_string;
}
elseif (($data['item_type'] ?? '') === 'book_chapter') {
$data['chapter_author'] = $author_string;
if (!empty($editors)) {
$data['author_editor'] = implode('; ', $editors);
}
}
else {
$data['author_editor'] = !empty($editors) ? implode('; ', $editors) : $author_string;
}
// Identifiers.
if (!empty($message['ISSN'])) {
$data['issn'] = $message['ISSN'][0];
}
if (!empty($message['ISBN'])) {
$data['isbn'] = $message['ISBN'][0];
}
// Volume, issue, pages.
if (!empty($message['volume'])) {
$data['volume'] = $message['volume'];
}
if (!empty($message['issue'])) {
$data['issue'] = $message['issue'];
}
if (!empty($message['page'])) {
$data['pages'] = $message['page'];
}
// Publication date.
$date_parts = $message['published']['date-parts'][0]
?? $message['published-print']['date-parts'][0]
?? $message['published-online']['date-parts'][0]
?? NULL;
if (!empty($date_parts[0])) {
$data['publication_date'] = (string) $date_parts[0];
}
// Publisher.
if (!empty($message['publisher'])) {
$data['publisher'] = $message['publisher'];
}
// Edition.
if (!empty($message['edition-number'])) {
$data['edition'] = $message['edition-number'];
}
return $data;
}
/**
* Injects DOI lookup data into the form state user input.
*
* This modifies the raw user input array so that when the form rebuilds,
* widget elements pick up the new values. Setting #default_value on form
* rebuild does not work because Drupal prioritizes cached user input.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
* @param array $data
* The parsed CrossRef data.
*/
function _ill_corporate_forms_inject_doi_values(FormStateInterface $form_state, array $data): void {
$field_map = [
'item_type' => 'field_ill_item_type',
'item_title' => 'field_ill_item_title',
'journal_title' => 'field_ill_journal_title',
'article_title' => 'field_ill_article_title',
'article_author' => 'field_ill_article_author',
'chapter_title' => 'field_ill_chapter_title',
'chapter_author' => 'field_ill_chapter_author',
'author_editor' => 'field_ill_author_editor',
'isbn' => 'field_ill_isbn',
'issn' => 'field_ill_issn',
'volume' => 'field_ill_volume',
'issue' => 'field_ill_issue',
'pages' => 'field_ill_pages',
'publication_date' => 'field_ill_publication_date',
'publisher' => 'field_ill_publisher',
'edition' => 'field_ill_edition',
];
$user_input = $form_state->getUserInput();
foreach ($field_map as $data_key => $field_name) {
if (empty($data[$data_key])) {
continue;
}
$value = $data[$data_key];
// Select/radio widgets use the field name directly in user input.
if ($field_name === 'field_ill_item_type') {
$user_input[$field_name] = $value;
}
else {
// Text field widgets use a nested array: field_name[0][value].
$user_input[$field_name][0]['value'] = $value;
}
}
$form_state->setUserInput($user_input);
}
/** /**
* Adds #states for item-type conditional fields on the request form. * Adds #states for item-type conditional fields on the request form.
* *

Loading…
Cancel
Save