diff --git a/composer.json b/composer.json index f233e794d6..8d07d17109 100644 --- a/composer.json +++ b/composer.json @@ -39,7 +39,7 @@ "drupal/access_unpublished": "1.6.0", "drupal/antibot": "2.0.4", "drupal/asset_injector": "2.21.0", - "drupal/auto_entitylabel": "3.3.0", + "drupal/auto_entitylabel": "3.4.0", "drupal/better_exposed_filters": "7.0.3", "drupal/blazy": "3.0.13", "drupal/block_class": "4.0.0", @@ -60,7 +60,7 @@ "drupal/config_split": "2.0.1", "drupal/config_sync": "3.0.0-alpha3", "drupal/config_update": "2.0.0-alpha4", - "drupal/core-recommended": "10.3.10", + "drupal/core-recommended": "10.3.11", "drupal/crop": "2.4.0", "drupal/ctools": "*", "drupal/date_ap_style": "2.0.2", diff --git a/modules/custom/az_person/az_person.module b/modules/custom/az_person/az_person.module index 5976efac6b..04d97a8a47 100644 --- a/modules/custom/az_person/az_person.module +++ b/modules/custom/az_person/az_person.module @@ -5,7 +5,9 @@ * Contains az_person.module. */ +use Drupal\Core\Entity\EntityFormInterface; use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Form\FormStateInterface; /** * Implements hook_preprocess_node(). @@ -34,6 +36,54 @@ function az_person_preprocess_views_view(&$variables) { } +/** + * Implements hook_form_FORM_ID_alter() for node_az_person_edit_form. + * + * Disables certain fields for imported person data. + */ +function az_person_form_node_az_person_edit_form_alter(&$form, FormStateInterface $form_state, $form_id) { + + $form_object = $form_state->getFormObject(); + if ($form_object instanceof EntityFormInterface) { + /** @var \Drupal\node\NodeInterface $node */ + $node = $form_object->getEntity(); + if ($node->hasField('field_az_netid')) { + $netid = $node->get('field_az_netid')->value; + if (!empty($netid)) { + $imported = []; + try { + // See if a migration map exists for this person. + $imported = \Drupal::service('migrate.lookup')->lookup('az_person_profiles_import', [$netid]); + } + catch (\Exception $e) { + // Migration did not exist, or migrate service not found. + // We have no data on this person being imported or not. + } + if (!empty($imported)) { + $person_warning = t('This person has been imported from the Profiles API.'); + \Drupal::messenger()->addWarning($person_warning); + $disabled_fields = [ + 'field_az_fname', + 'field_az_lname', + 'field_az_netid', + 'field_az_email', + 'field_az_phones', + 'field_az_titles', + 'field_az_degrees', + 'field_az_address', + 'field_az_body', + ]; + foreach ($disabled_fields as $field) { + if (!empty($form[$field])) { + $form[$field]['#disabled'] = TRUE; + } + } + } + } + } + } +} + /** * Implements hook_entity_bundle_field_info_alter(). */ diff --git a/modules/custom/az_person/az_person_profiles_import/az_person_profiles_import.info.yml b/modules/custom/az_person/az_person_profiles_import/az_person_profiles_import.info.yml new file mode 100644 index 0000000000..9eaf441bcf --- /dev/null +++ b/modules/custom/az_person/az_person_profiles_import/az_person_profiles_import.info.yml @@ -0,0 +1,14 @@ +name: 'Quickstart Person Profiles Import' +type: module +description: 'Import people via UA Vitae Profiles.' +core_version_requirement: ^9 || ^10 +package: 'The University of Arizona - Experimental' +dependencies: + - az_person + - field + - migrate + - migrate_plus + - migrate_tools + - node + - path + - pathauto diff --git a/modules/custom/az_person/az_person_profiles_import/az_person_profiles_import.links.task.yml b/modules/custom/az_person/az_person_profiles_import/az_person_profiles_import.links.task.yml new file mode 100644 index 0000000000..b7170436da --- /dev/null +++ b/modules/custom/az_person/az_person_profiles_import/az_person_profiles_import.links.task.yml @@ -0,0 +1,12 @@ +az_person_profiles_import.import_tab: + title: 'Import Person' + route_name: az_person_profiles_import.form + parent_id: system.admin_content + description: 'Import a person from the Profiles API' + weight: 102 + +az_person_profiles_import.settings_tab: + route_name: az_person_profiles_import.settings_form + title: 'Profiles Integration' + base_route: az_core.az_settings + weight: 6 \ No newline at end of file diff --git a/modules/custom/az_person/az_person_profiles_import/az_person_profiles_import.module b/modules/custom/az_person/az_person_profiles_import/az_person_profiles_import.module new file mode 100644 index 0000000000..6a0cde45f4 --- /dev/null +++ b/modules/custom/az_person/az_person_profiles_import/az_person_profiles_import.module @@ -0,0 +1,52 @@ +bundle() === 'az_person') && $entity->isSyncing() && !$entity->isNew()) { + // Create new revision on syncing person records. + $revision_log = 'Updated from profiles import.'; + foreach ($fields as $field) { + if ($entity->hasField($field) && !empty($entity->original)) { + // Check if field changed. + $value = $entity->get($field)->getValue(); + $original = $entity->original->get($field)->getValue(); + if (serialize($value) !== serialize($original)) { + // Field value has changed. + // Get human readable name of field. + $field_info = FieldStorageConfig::loadByName('node', $field); + if (!is_null($field_info)) { + $label = $field_info->getLabel(); + // Add mention of field being changed in log message. + $revision_log .= " Modified field " . $label . '.'; + } + } + } + } + $entity->setNewRevision(TRUE); + $entity->setRevisionLogMessage($revision_log); + $entity->isDefaultRevision(TRUE); + } +} diff --git a/modules/custom/az_person/az_person_profiles_import/az_person_profiles_import.routing.yml b/modules/custom/az_person/az_person_profiles_import/az_person_profiles_import.routing.yml new file mode 100644 index 0000000000..23d180915d --- /dev/null +++ b/modules/custom/az_person/az_person_profiles_import/az_person_profiles_import.routing.yml @@ -0,0 +1,15 @@ +az_person_profiles_import.settings_form: + path: '/admin/config/az-quickstart/settings/az-person-profiles-import' + defaults: + _title: 'Profiles Import Settings' + _form: 'Drupal\az_person_profiles_import\Form\AZPersonProfilesImportSettingsForm' + requirements: + _permission: 'administer site configuration' + +az_person_profiles_import.form: + path: '/admin/content/profiles/import' + defaults: + _title: 'Import Profiles' + _form: 'Drupal\az_person_profiles_import\Form\AZPersonProfilesImportForm' + requirements: + _permission: 'create az_person content' diff --git a/modules/custom/az_person/az_person_profiles_import/az_person_profiles_import.services.yml b/modules/custom/az_person/az_person_profiles_import/az_person_profiles_import.services.yml new file mode 100644 index 0000000000..0441bdd274 --- /dev/null +++ b/modules/custom/az_person/az_person_profiles_import/az_person_profiles_import.services.yml @@ -0,0 +1,8 @@ +services: + _defaults: + autoconfigure: true + az_person_profiles_import_subscriber: + class: Drupal\az_person_profiles_import\EventSubscriber\AZPersonProfilesImportEventSubscriber + arguments: + - '@messenger' + - '@entity_type.manager' diff --git a/modules/custom/az_person/az_person_profiles_import/config/install/az_person_profiles_import.settings.yml b/modules/custom/az_person/az_person_profiles_import/config/install/az_person_profiles_import.settings.yml new file mode 100644 index 0000000000..6d0f7f1fe0 --- /dev/null +++ b/modules/custom/az_person/az_person_profiles_import/config/install/az_person_profiles_import.settings.yml @@ -0,0 +1,2 @@ +endpoint: 'https://profiles-api.arizona.edu' +apikey: '' diff --git a/modules/custom/az_person/az_person_profiles_import/config/schema/az_person_profiles_import.schema.yml b/modules/custom/az_person/az_person_profiles_import/config/schema/az_person_profiles_import.schema.yml new file mode 100644 index 0000000000..d863bb2dac --- /dev/null +++ b/modules/custom/az_person/az_person_profiles_import/config/schema/az_person_profiles_import.schema.yml @@ -0,0 +1,11 @@ +# Schema for the configuration files of the Quickstart Person Profiles Import module. +az_person_profiles_import.settings: + type: config_object + label: 'Quickstart Person Profiles Import settings' + mapping: + endpoint: + type: string + label: 'Profiles Endpoint' + apikey: + type: string + label: 'API Key' diff --git a/modules/custom/az_person/az_person_profiles_import/migrations/az_person_profiles_import.yml b/modules/custom/az_person/az_person_profiles_import/migrations/az_person_profiles_import.yml new file mode 100644 index 0000000000..c9ac67b27c --- /dev/null +++ b/modules/custom/az_person/az_person_profiles_import/migrations/az_person_profiles_import.yml @@ -0,0 +1,112 @@ +id: az_person_profiles_import +label: Profiles Integration +migration_tags: + - Profiles Integration +source: + plugin: url + data_fetcher_plugin: az_profiles_api_fetcher + data_parser_plugin: az_person_profiles_import_json + urls: [] + item_selector: / + ids: + netid: + type: string + constants: + NAME: 'name' + + fields: + - + name: netid + selector: 'Person/netid' + - + name: surname + selector: 'Person/surname' + - + name: givenname + selector: 'Person/givenname' + - + name: email + selector: 'Person/email' + - + name: biography + selector: 'Bio/desc' + - + name: titles + selector: 'Titles' + - + name: phone + selector: 'Person/phone' + - + name: office + selector: 'Person/office' + - + name: degrees + selector: 'Degrees' + +process: + nid: + - + plugin: entity_lookup + entity_type: node + bundle_key: type + bundle: az_person + value_key: field_az_netid + source: netid + - + plugin: skip_on_empty + method: process + type: + plugin: default_value + default_value: az_person + field_az_fname: givenname + field_az_lname: surname + field_az_netid: netid + field_az_email: + - + plugin: skip_on_empty + method: row + source: email + message: 'is unavailable in the profiles API' + - + plugin: get + field_az_body/value: biography + field_az_body/format: + plugin: default_value + default_value: az_standard + field_az_titles: + plugin: sub_process + source: titles + process: + value: desc + field_az_degrees: + plugin: az_person_degrees + source: degrees + field_az_phones: + plugin: sub_process + source: phone + process: + value: number + field_az_address: + - + plugin: sub_process + source: office + process: + 0: + plugin: concat + source: + - building_name + - room_nbr + delimiter: ' ' + - + plugin: flatten + - + plugin: concat + delimiter: "\n" +destination: + plugin: entity:node + bundle: az_person + +dependencies: + enforced: + module: + - az_person diff --git a/modules/custom/az_person/az_person_profiles_import/src/EventSubscriber/AZPersonProfilesImportEventSubscriber.php b/modules/custom/az_person/az_person_profiles_import/src/EventSubscriber/AZPersonProfilesImportEventSubscriber.php new file mode 100644 index 0000000000..9c52e77801 --- /dev/null +++ b/modules/custom/az_person/az_person_profiles_import/src/EventSubscriber/AZPersonProfilesImportEventSubscriber.php @@ -0,0 +1,94 @@ +messenger = $messenger; + $this->entityTypeManager = $entityTypeManager; + } + + /** + * Get subscribed events. + * + * @inheritdoc + */ + public static function getSubscribedEvents() { + $events = []; + $events[MigrateEvents::POST_ROW_SAVE] = ['onPostRowSave']; + $events[MigrateEvents::IDMAP_MESSAGE] = ['onMapMessage']; + return $events; + } + + /** + * Respond to events on migration message. + * + * @param \Drupal\migrate\Event\MigrateIdMapMessageEvent $event + * The map message event object. + */ + public function onMapMessage(MigrateIdMapMessageEvent $event) { + $migration = $event->getMigration()->getBaseId(); + // Only emit warnings for the profiles import. + if ($migration === 'az_person_profiles_import') { + $sourceIds = $event->getSourceIdValues(); + $netid = $sourceIds['netid'] ?? ''; + $message = $event->getMessage(); + // Consume name of migration and field that prepends message. + $message = preg_replace('/^.*:.*: /', '', $message); + // Output the migration message. + $this->messenger->addWarning(t('NetID %netid @message.', ['%netid' => $netid, '@message' => $message])); + } + } + + /** + * Respond to events on migration import for relevant migrations. + * + * @param \Drupal\migrate\Event\MigratePostRowSaveEvent $event + * The post save event object. + */ + public function onPostRowSave(MigratePostRowSaveEvent $event) { + $migration = $event->getMigration()->getBaseId(); + $ids = $event->getDestinationIdValues(); + $id = reset($ids); + if ($migration === 'az_person_profiles_import') { + $person = $this->entityTypeManager->getStorage('node')->load($id); + if (!empty($person)) { + $url = $person->toUrl()->toString(); + $this->messenger->addMessage(t('Imported @name.', [ + '@link' => $url, + '@name' => $person->getTitle(), + ])); + } + } + } + +} diff --git a/modules/custom/az_person/az_person_profiles_import/src/Form/AZPersonProfilesImportForm.php b/modules/custom/az_person/az_person_profiles_import/src/Form/AZPersonProfilesImportForm.php new file mode 100644 index 0000000000..e782565b56 --- /dev/null +++ b/modules/custom/az_person/az_person_profiles_import/src/Form/AZPersonProfilesImportForm.php @@ -0,0 +1,141 @@ +messenger = $container->get('messenger'); + $instance->pluginManagerMigration = $container->get('plugin.manager.migration'); + return $instance; + } + + /** + * {@inheritdoc} + */ + public function getFormId(): string { + return 'az_person_profiles_import'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state): array { + + $config = $this->config('az_person_profiles_import.settings'); + $has_key = !empty(trim($config->get('apikey'))); + if (!$has_key) { + $url = Url::fromRoute('az_person_profiles_import.settings_form')->toString(); + $this->messenger->addWarning($this->t('You must first configure a Profiles API token here.', [ + ':link' => $url, + ])); + } + + $form['netid'] = [ + '#type' => 'textarea', + '#title' => $this->t('List of NetID(s)'), + '#description' => $this->t('Enter the NetIDs of the individuals you wish to import, one per line.'), + '#disabled' => !$has_key, + '#required' => TRUE, + ]; + + $form['mode'] = [ + '#type' => 'select', + '#title' => $this->t('Choose how profiles are imported'), + '#options' => [ + 'normal' => $this->t('Import new profiles only'), + 'track_changes' => $this->t('Import new profiles and profiles updated since the last import'), + 'update' => $this->t('Import all listed profiles'), + ], + '#disabled' => !$has_key, + '#required' => TRUE, + ]; + + $form['actions'] = [ + '#type' => 'actions', + '#disabled' => !$has_key, + 'submit' => [ + '#type' => 'submit', + '#value' => $this->t('Import'), + ], + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state): void { + $urls = []; + $netids = $form_state->getValue('netid'); + $mode = $form_state->getValue('mode'); + $netids = preg_split("(\r\n?|\n)", $netids); + $update = $mode === 'update'; + $track = $mode === 'track_changes'; + + foreach ($netids as $netid) { + // For the profiles API fetcher, the url is the netid. + $netid = trim($netid); + $urls[] = $netid; + } + + // Fetch the profiles integration migration. + $migration = $this->pluginManagerMigration->createInstance('az_person_profiles_import'); + // Phpstan doesn't know this can be NULL. + // @phpstan-ignore-next-line + if (!empty($migration)) { + // Reset status. + $status = $migration->getStatus(); + if ($status !== MigrationInterface::STATUS_IDLE) { + $migration->setStatus(MigrationInterface::STATUS_IDLE); + } + // Set migration options. + $options = [ + 'limit' => 0, + 'update' => (int) $update, + 'track_changes' => (int) $track, + 'configuration' => [ + 'source' => [ + 'urls' => $urls, + ], + ], + ]; + + // Run the migration. + $executable = new MigrateBatchExecutable($migration, new MigrateMessage(), $options); + $executable->batchImport(); + } + } + +} diff --git a/modules/custom/az_person/az_person_profiles_import/src/Form/AZPersonProfilesImportSettingsForm.php b/modules/custom/az_person/az_person_profiles_import/src/Form/AZPersonProfilesImportSettingsForm.php new file mode 100644 index 0000000000..625f247f10 --- /dev/null +++ b/modules/custom/az_person/az_person_profiles_import/src/Form/AZPersonProfilesImportSettingsForm.php @@ -0,0 +1,63 @@ +config('az_person_profiles_import.settings'); + $form['endpoint'] = [ + '#type' => 'url', + '#title' => $this->t('Profiles API Endpoint'), + '#description' => $this->t('Enter a fully qualified URL for the endpoint of the profiles API service.'), + '#default_value' => $config->get('endpoint'), + '#required' => TRUE, + ]; + $form['apikey'] = [ + '#type' => 'password', + '#title' => $this->t('API Token'), + '#description' => $this->t('Enter an API Token for the profiles API service.'), + '#maxlength' => 128, + '#size' => 64, + '#default_value' => $config->get('apikey'), + ]; + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state): void { + $this->config('az_person_profiles_import.settings') + ->set('endpoint', $form_state->getValue('endpoint')) + ->set('apikey', $form_state->getValue('apikey')) + ->save(); + parent::submitForm($form, $form_state); + } + +} diff --git a/modules/custom/az_person/az_person_profiles_import/src/Plugin/migrate/process/AZPersonDegrees.php b/modules/custom/az_person/az_person_profiles_import/src/Plugin/migrate/process/AZPersonDegrees.php new file mode 100644 index 0000000000..67c04ae42d --- /dev/null +++ b/modules/custom/az_person/az_person_profiles_import/src/Plugin/migrate/process/AZPersonDegrees.php @@ -0,0 +1,57 @@ + implode(', ', $d)]; + } + + return $degrees; + } + +} diff --git a/modules/custom/az_person/az_person_profiles_import/src/Plugin/migrate_plus/data_fetcher/AZProfilesAPIFetcher.php b/modules/custom/az_person/az_person_profiles_import/src/Plugin/migrate_plus/data_fetcher/AZProfilesAPIFetcher.php new file mode 100644 index 0000000000..3700668091 --- /dev/null +++ b/modules/custom/az_person/az_person_profiles_import/src/Plugin/migrate_plus/data_fetcher/AZProfilesAPIFetcher.php @@ -0,0 +1,94 @@ +httpClient = $container->get('az_http.http_client'); + } + catch (ServiceNotFoundException $e) { + // Otherwise, fall back on the Drupal core guzzle client. + $instance->httpClient = $container->get('http_client'); + } + $instance->configFactory = $container->get('config.factory'); + return $instance; + } + + /** + * {@inheritdoc} + */ + public function getResponseContent(string $url): string { + // Grab the profiles API settings from configuration. + $config = $this->configFactory->get('az_person_profiles_import.settings'); + $endpoint = $config->get('endpoint'); + $apikey = $config->get('apikey'); + + // For this fetcher, the supplied URL is the netid. + $netid = $url; + // Construct the API call. + $url = $endpoint . '/get/' . urlencode($netid) . '?apikey=' . urlencode($apikey); + try { + $body = (string) $this->getResponse($url)->getBody(); + } + catch (MigrateException | RequestException $e) { + // Response from API had no data. + $json = ['Person' => ['netid' => $netid]]; + $body = json_encode($json); + } + return $body; + } + + /** + * {@inheritdoc} + */ + public function getNextUrls(string $url): array { + return []; + } + +} diff --git a/modules/custom/az_person/az_person_profiles_import/src/Plugin/migrate_plus/data_parser/AZPersonProfilesImportJson.php b/modules/custom/az_person/az_person_profiles_import/src/Plugin/migrate_plus/data_parser/AZPersonProfilesImportJson.php new file mode 100644 index 0000000000..4b5da99b11 --- /dev/null +++ b/modules/custom/az_person/az_person_profiles_import/src/Plugin/migrate_plus/data_parser/AZPersonProfilesImportJson.php @@ -0,0 +1,54 @@ +