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 @@
+