|
|
|
|
@ -6,12 +6,14 @@
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
|
|
use Drupal\Core\Form\FormStateInterface; |
|
|
|
|
use GuzzleHttp\Exception\GuzzleException; |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 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 { |
|
|
|
|
_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_institution_code'; |
|
|
|
|
$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 { |
|
|
|
|
_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_institution_code'; |
|
|
|
|
$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; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 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. |
|
|
|
|
* |
|
|
|
|
|