From 683bcbca4dd8241e61d9bcb4d44ade8b0fc1640a Mon Sep 17 00:00:00 2001 From: Diego Pino Navarro Date: Mon, 2 Aug 2021 14:04:05 -0400 Subject: [PATCH 1/5] Allow Draft Sessions to Survive Editing/Disable Drafts for Cloning This basically discerns if an edit session can use Drafts (e.g Cloning can not) and we keep Draft sessions separated from normal advance sessions. Reason for that is we need to be able to Edit a Node without touching an ongoing/Draft session too. We use the same widget ID as temporary private store storage but we add a -draft Also: - Marks Draft as Saved (message( with normal Operations (next/previous buttons) by setting $form_state->set('draft_saved', TRUE); Those buttons still validate the form v/s the Top navigation one. - Add the Step number in the message when reaching the last step and validation for previous ones fails --- composer.json | 1 + .../StrawberryRunnerModalController.php | 7 ++-- ...WebformStrawberryfieldDeleteTmpStorage.php | 9 ++++- .../StrawberryFieldWebFormInlineWidget.php | 39 ++++++++++++------- .../strawberryFieldharvester.php | 33 +++++++++------- 5 files changed, 57 insertions(+), 32 deletions(-) diff --git a/composer.json b/composer.json index b4caf69..f392b9b 100644 --- a/composer.json +++ b/composer.json @@ -14,6 +14,7 @@ ], "require": { "php": ">=7.2", + "ext-json": "*", "ml/json-ld": "^1.2", "mtdowling/jmespath.php":"^2.5", "strawberryfield/strawberryfield":"dev-1.0.0-RC2", diff --git a/src/Controller/StrawberryRunnerModalController.php b/src/Controller/StrawberryRunnerModalController.php index 8feb4dd..09fc623 100644 --- a/src/Controller/StrawberryRunnerModalController.php +++ b/src/Controller/StrawberryRunnerModalController.php @@ -138,17 +138,18 @@ public function openModalForm(WebformInterface $webform = NULL, Request $request // In case we are editing an existing entity, this one gets the // Strawberryfield value $alldata = $source_entity->get($field_name)->getValue(); - $fielddata['value'] = !empty($alldata) ? $alldata[$delta]['value']: "{}"; + $fielddata['value'] = $alldata[$delta]['value'] ?? "{}"; $entityid = $source_entity->id(); } - $stored_value = (isset($fielddata['value']) && !empty($fielddata['value'])) ? $fielddata['value'] : "{}"; + $stored_value = $fielddata['value'] ?? "{}"; $data_defaults = [ 'strawberry_field_widget_state_id' => $widgetid, // Can't remember why, but seems useful to pass around 'strawberry_field_widget_source_entity_uuid' => $source_uuid, 'strawberry_field_widget_source_entity_id' => $entityid, + 'strawberry_field_widget_autosave' => $entityid ? FALSE : TRUE, 'strawberry_field_stored_values' => json_decode($stored_value,true) ]; @@ -247,7 +248,7 @@ public function openModalForm(WebformInterface $webform = NULL, Request $request // We delete both, the session and the accumulated errors. /** @var \Drupal\Core\TempStore\PrivateTempStore $tempstore */ $tempstore = \Drupal::service('tempstore.private')->get('archipel'); - $tempstore->delete($clear_saved); + $tempstore->delete($clear_saved.'-draft'); $tempstore->delete($clear_saved.'-errors'); // Selector us built using the field name and the delta. $response->addCommand(new \Drupal\Core\Ajax\HtmlCommand('#' . $selector .' > .fieldset-wrapper', diff --git a/src/EventSubscriber/WebformStrawberryfieldDeleteTmpStorage.php b/src/EventSubscriber/WebformStrawberryfieldDeleteTmpStorage.php index b50bc3f..d5a260d 100644 --- a/src/EventSubscriber/WebformStrawberryfieldDeleteTmpStorage.php +++ b/src/EventSubscriber/WebformStrawberryfieldDeleteTmpStorage.php @@ -111,8 +111,13 @@ public function onEntityInsert(StrawberryfieldCrudEvent $event) { foreach ($field->getValue() as $delta => $value) { $keyid = $this->getTempStoreKeyName($fieldname, $delta, ''); $tempstore->delete($keyid); - // Delete also any cached errors - $tempstore->delete($keyid.'-errors'); + // Delete also any cached errors and drafts if the autosave session + // Generated this ADO. If not (e.g Clone we will have marked that + // ADO's UUID as not autosave so we do not delete another ongoing session + if ($this->tempStoreFactory->get('archipel_autosave')->get($entity->uuid())) { + $tempstore->delete($keyid . '-errors'); + $tempstore->delete($keyid . '-draft'); + } } } diff --git a/src/Plugin/Field/FieldWidget/StrawberryFieldWebFormInlineWidget.php b/src/Plugin/Field/FieldWidget/StrawberryFieldWebFormInlineWidget.php index 035a152..d514896 100644 --- a/src/Plugin/Field/FieldWidget/StrawberryFieldWebFormInlineWidget.php +++ b/src/Plugin/Field/FieldWidget/StrawberryFieldWebFormInlineWidget.php @@ -12,11 +12,13 @@ use Drupal\Core\Ajax\AjaxResponse; use Drupal\Core\Ajax\HtmlCommand; use Drupal\Core\Entity\EntityMalformedException; +use Drupal\Core\Entity\Plugin\DataType\EntityAdapter; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Field\WidgetBase; use Drupal\Core\Url; +use Drupal\node\Entity\Node; use Symfony\Component\DependencyInjection\ContainerInterface; use Drupal\webform\WebformInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; @@ -265,7 +267,6 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen 'strawberry_webform_inline', ]); $limit_validation_errors = $parents; - // We add 'data-drupal-selector' = 'strawberry_webform_widget' // To allow JS to react/jquery select on this. $element += [ @@ -292,24 +293,23 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen // Which means an abandoned Metadata Sessions somewhere // Someone saved/drafted 'metadata' during a form session and left for coffee // WE can reuse! - - if (($tempstore->getMetadata($tempstoreId) != NULL) && $items->getEntity() - ->isNew()) { + $default_value = $items->getFieldDefinition()->getDefaultValue($items->getEntity()); + $default_value = $default_value[$delta]['value'] ?? "{}"; + $autosave = ($items->getEntity()->isNew() && $savedvalue['value'] == $default_value); + // If the SBF in this entity has data and its different to the default for + // the field then we are sure it can not be auto saved (e.g when cloning) + // Not should we load a session. + if (($tempstore->getMetadata($tempstoreId.'-draft') != NULL) && $autosave) { $discard = $form_state->getUserInput()['_triggering_element_name'] ?? FALSE; $discard = $discard == 'webform_strawberryfield_discard_session' ?? FALSE; - - $json_string = $tempstore->get($tempstoreId); + $autosave = TRUE; + $json_string = $tempstore->get($tempstoreId.'-draft'); $json = json_decode($json_string, TRUE); $json_error = json_last_error(); if ($json_error == JSON_ERROR_NONE) { $savedvalue['value'] = $json_string; - $element['strawberry_webform_inline_message'] = [ - '#id' => 'ajax-value', - '#theme' => 'status_messages', - '#message_list' => [ - 'status' => [$this->t('We found and loaded a previous unfinished metadata session for you.')], - ], - ]; + // Let's use the time to see if draft was changed before or after this call + // Since this form rebuilds itself with any webform ajax interaction $webform_controller_url_clear = Url::fromRoute('webform_strawberryfield.modal_webform', [ @@ -320,9 +320,11 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen 'clear_saved' => $tempstoreId, ] ); + $element['strawberry_webform_discard_session'] = [ '#type' => 'link', '#title' => $this->t('Discard Session'), + '#description' => $this->t('We found and unfinished Metadata Session'), '#url' => $webform_controller_url_clear, '#attributes' => [ 'class' => [ @@ -333,9 +335,12 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen ], ], ]; + $element['#title'] = $element['#title'] . $this->t(' (Unfinished Metadata Session loaded)'); } } + $form_state->set('autosave', $autosave); + // If new this won't exist $stored_value = !empty($savedvalue['value']) ? $savedvalue['value'] : "{}"; @@ -343,6 +348,7 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen 'strawberry_field_widget_state_id' => $this_widget_id, 'strawberry_field_widget_source_entity_uuid' => $entity_uuid, 'strawberry_field_widget_source_entity_id' => $entity_id, + 'strawberry_field_widget_autosave' => $autosave, 'strawberry_field_stored_values' => json_decode($stored_value, TRUE), ]; @@ -426,7 +432,7 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen ], ]; } - if ($this->getSetting('hide_cancel') === FALSE || $this->getSetting('hide_cancel') == NULL) { + if (($this->getSetting('hide_cancel') === FALSE || $this->getSetting('hide_cancel') == NULL) && $autosave == FALSE ){ $webform_controller_url_close = Url::fromRoute('webform_strawberryfield.close_modal_webform', [ 'state' => "$entity_uuid:$this_field_name:$delta:$this_widget_id", @@ -538,6 +544,11 @@ public function validateWebform($element, FormStateInterface $form_state) { } $json_string = $tempstore->get($tempstoreId); + $autosave = $form_state->get('autosave'); + $tempstore_noautosave = \Drupal::service('tempstore.private')->get('archipel_autosave'); + $final_uuid = $form_state->getFormObject()->getEntity()->uuid(); + $tempstore_noautosave->set($final_uuid, $autosave); + $json = json_decode($json_string, TRUE); $json_error = json_last_error(); if ($json_error == JSON_ERROR_NONE) { diff --git a/src/Plugin/WebformHandler/strawberryFieldharvester.php b/src/Plugin/WebformHandler/strawberryFieldharvester.php index d67b17c..c890da2 100644 --- a/src/Plugin/WebformHandler/strawberryFieldharvester.php +++ b/src/Plugin/WebformHandler/strawberryFieldharvester.php @@ -557,14 +557,16 @@ public function preSave(WebformSubmissionInterface $webform_submission) { ); } try { + // Saves all what was done in the webform in our tempstore + // To be later retrieved by the Field Widget Submit $tempstore->set( $values["strawberry_field_widget_state_id"], $cleanvalues ); // Just in case we have stashed errors remove them $tempstore->delete($values['strawberry_field_widget_state_id'] . '-errors'); - - } catch (TempStoreException $e) { + } + catch (TempStoreException $e) { $this->messenger()->addError( $this->t( 'Sorry, we have issues writing metadata to your session storage. Please reload this form and/or contact your system admin.' @@ -609,8 +611,8 @@ public function validateForm( $values = $webform_submission->getData(); // So we can now unset cached errors on this step. Only if triggered by next here. // Wizard navigation has its own way. - if ((!isset($values['strawberry_field_widget_source_entity_id']) || - $values['strawberry_field_widget_source_entity_id'] === NULL) && + if ((!isset($values['strawberry_field_widget_autosave']) || + $values['strawberry_field_widget_autosave'] === TRUE) && isset($values['strawberry_field_widget_state_id'])) { if (isset($form_state->getTriggeringElement()['#name']) && $form_state->getTriggeringElement()['#name'] == 'op') { $current_page = $webform_submission->getCurrentPage(); @@ -666,10 +668,11 @@ public function submitForm( /* @var $tempstore \Drupal\Core\TempStore\PrivateTempStore */ $tempstore = \Drupal::service('tempstore.private')->get('archipel'); $tempstore->set( - $values['strawberry_field_widget_state_id'], + $values['strawberry_field_widget_state_id'].'-draft', $cleanvalues ); $form_state->set('in_draft', TRUE); + $form_state->set('draft_saved', TRUE); } catch (TempStoreException $e) { $this->messenger()->addError( $this->t( @@ -829,8 +832,8 @@ public function alterForm(array &$form, FormStateInterface $form_state, WebformS // Instead of validating previous invisible steps and setting errors here // we will block the submit button until all errors are cleared. - if ((!isset($values['strawberry_field_widget_source_entity_id']) || - $values['strawberry_field_widget_source_entity_id'] === NULL) && + if ((!isset($values['strawberry_field_widget_autosave']) || + $values['strawberry_field_widget_autosave'] === TRUE) && isset($values['strawberry_field_widget_state_id']) ) { $tempstore = \Drupal::service('tempstore.private')->get('archipel'); @@ -845,7 +848,9 @@ public function alterForm(array &$form, FormStateInterface $form_state, WebformS $can_not = FALSE; if (is_array($previous_errors)) { + $i = 0; foreach ($all_pages as $pagekey => $pagekeyinfo) { + $i++; if (($pagekeyinfo['#access'] === TRUE) && isset($previous_errors[$pagekey]) && is_array($previous_errors[$pagekey]) && @@ -854,8 +859,9 @@ public function alterForm(array &$form, FormStateInterface $form_state, WebformS $this->messenger() ->addWarning(t('You can not submit this form yet.'), FALSE); $this->messenger() - ->addWarning(t('Please check the @steps step for missing or incorrect fields', [ + ->addWarning(t('Please check the @steps step (@number) for missing or incorrect fields', [ '@steps' => $pagekeyinfo['#title'], + '@number' => $i, ])); } } @@ -864,8 +870,8 @@ public function alterForm(array &$form, FormStateInterface $form_state, WebformS } } } - } elseif ((!isset($values['strawberry_field_widget_source_entity_id']) || - $values['strawberry_field_widget_source_entity_id'] === NULL) && + } elseif ((!isset($values['strawberry_field_widget_autosave']) || + $values['strawberry_field_widget_autosave'] === TRUE) && isset($values['strawberry_field_widget_state_id']) && isset($form['pages']) ) { @@ -904,8 +910,8 @@ public function sbfDraftValidate(array &$form, FormStateInterface $form_state) { } $values = $this->getWebformSubmission()->getData(); // Only allow saving of drafts if the user is creating a new entity - if ((!isset($values['strawberry_field_widget_source_entity_id']) || - $values['strawberry_field_widget_source_entity_id'] === NULL) && + if ((!isset($values['strawberry_field_widget_autosave']) || + $values['strawberry_field_widget_autosave'] === TRUE) && isset($values['strawberry_field_widget_state_id']) ) { $this->setIsWidgetDriven(TRUE); @@ -927,7 +933,8 @@ public function sbfDraftValidate(array &$form, FormStateInterface $form_state) { $form_state->set('in_draft', TRUE); $form_state->set('draft_saved', TRUE); $this->getWebformSubmission()->validate(); - } catch (TempStoreException $e) { + } + catch (TempStoreException $e) { $this->messenger()->addError( $this->t( 'Sorry, we have issues writing metadata to your session storage. Please reload this form and/or contact your system admin.' From ff96c700b586b8582a170987ba85e1514990c5d1 Mon Sep 17 00:00:00 2001 From: Diego Pino Navarro Date: Mon, 2 Aug 2021 14:04:34 -0400 Subject: [PATCH 2/5] Be less strict when checking for internal calls on Auth Autocomplete controllers without validation/user session readable --- src/Controller/AuthAutocompleteController.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Controller/AuthAutocompleteController.php b/src/Controller/AuthAutocompleteController.php index a443a91..6b17a39 100644 --- a/src/Controller/AuthAutocompleteController.php +++ b/src/Controller/AuthAutocompleteController.php @@ -163,6 +163,9 @@ public function handleAutocomplete(Request $request, $auth_type, $vocab = 'subje if (is_string($csrf_token)) { $request_base = $request->getSchemeAndHttpHost().':'.$request->getPort(); $is_internal = $_SERVER['REQUEST_SCHEME'].'://'.$_SERVER['SERVER_ADDR'].':'.$_SERVER['SERVER_PORT'] == $request_base; + if (!$is_internal) { + $is_internal = $_SERVER['HTTP_HOST'] == $_SERVER['SERVER_NAME']; + } } if ($input) { @@ -612,12 +615,12 @@ protected function viaf($input) { $jsondata = []; $results = []; - $jsondata = json_decode($body, TRUE); + $jsondata = json_decode($body, TRUE) ?? []; $json_error = json_last_error(); if ($json_error == JSON_ERROR_NONE) { //WIKIdata will give is an success key will always return at least one, the query string if (count($jsondata) > 1) { - if (count($jsondata['result']) >= 1) { + if (isset($jsondata['result']) && is_array($jsondata['result']) && count($jsondata['result']) >= 1) { foreach ($jsondata['result'] as $key => $item) { $desc = (isset($item['nametype'])) ? '(' . $item['nametype'] . ')' : NULL; $results[] = [ From 0938706fcd701f1b6469ad43828d60258b8a321d Mon Sep 17 00:00:00 2001 From: Diego Pino Navarro Date: Tue, 3 Aug 2021 16:12:48 -0400 Subject: [PATCH 3/5] Add Snac --- src/Controller/AuthAutocompleteController.php | 107 ++++++++++- src/Element/WebformSnac.php | 114 ++++++++++++ src/Plugin/WebformElement/WebformSnac.php | 174 ++++++++++++++++++ 3 files changed, 392 insertions(+), 3 deletions(-) create mode 100644 src/Element/WebformSnac.php create mode 100644 src/Plugin/WebformElement/WebformSnac.php diff --git a/src/Controller/AuthAutocompleteController.php b/src/Controller/AuthAutocompleteController.php index 6b17a39..48b0e36 100644 --- a/src/Controller/AuthAutocompleteController.php +++ b/src/Controller/AuthAutocompleteController.php @@ -16,6 +16,7 @@ use GuzzleHttp\Client; use GuzzleHttp\Exception\ClientException; use GuzzleHttp\Exception\ServerException; +use GuzzleHttp\RequestOptions; use Drupal\Component\Utility\UrlHelper; use Drupal\Core\Url; use Drupal\Component\Datetime\TimeInterface; @@ -210,6 +211,10 @@ public function handleAutocomplete(Request $request, $auth_type, $vocab = 'subje ) ); } + break; + case 'snac': + $results = $this->snac($input, $vocab, $rdftype); + break; } } // DO not cache NULL or FALSE. Those will be 401/403/500; @@ -764,6 +769,91 @@ protected function europeana($input, $vocab, string $apikey) { return []; } + /** + * @param $input + * The query + * @param $vocab + * The 'suggest' enabled endpoint at LoC + * + * @return array + */ + protected function snac($input, $vocab, $rdftype) { + //@TODO make the following allowed list a constant since we use it in + // \Drupal\webform_strawberryfield\Plugin\WebformElement\WebformLoC + if (!in_array($vocab, [ + 'Constellation', + 'rdftype', + ])) { + // Drop before tryin to hit non existing vocab + $this->messenger()->addError( + $this->t('@vocab for SNAC autocomplete is not in in our allowed list.', + [ + '@vocab' => $vocab, + ] + ) + ); + $results[] = [ + 'value' => NULL, + 'label' => "Wrong Vocabulary {$vocab} in SNAC Query", + ]; + return $results; + } + + + $input = urlencode($input); + + + $remoteUrl = "https://api.snaccooperative.org"; + $options = [ + 'body' => json_encode([ + "command" => "search", + "term" => $input, + "entity_type" => $rdftype != "thing" ? $rdftype : NULL, + "start" => 0, + "count" => 10, + "search_type" => "autocomplete", + ]), + RequestOptions::HEADERS => [ + 'Content-Type' => 'application/json' + ] + ]; + $body = $this->getRemoteJsonData($remoteUrl, $options, 'PUT'); + + $jsondata = []; + $results = []; + $jsondata = json_decode($body, TRUE); + $json_error = json_last_error(); + if ($json_error == JSON_ERROR_NONE) { + if (!empty($jsondata['results']) && ($jsondata['total'] ?? 0) >= 1) { + foreach ($jsondata['results'] as $key => $entry) { + $nameEntry = reset($entry['nameEntries']); + $results[] = [ + 'value' => $entry['ark'] ?? $entry['entityType']['uri'], + 'label' => $nameEntry['original'], + ]; + } + } + else { + $results[] = [ + 'value' => NULL, + 'label' => "Sorry no match from SNAC {$vocab}", + ]; + } + return $results; + } + $this->messenger()->addError( + $this->t('Looks like data fetched from @url is not in JSON format.
JSON says: @jsonerror
Please check your URL!', + [ + '@url' => $remoteUrl, + '@jsonerror' => $json_error, + ] + ) + ); + return []; + } + + + /** * @param $remoteUrl * @param $options @@ -771,7 +861,7 @@ protected function europeana($input, $vocab, string $apikey) { * @return string * A string that may be JSON (hopefully) */ - protected function getRemoteJsonData($remoteUrl, $options) { + protected function getRemoteJsonData($remoteUrl, $options, $method = 'GET') { // This is expensive, reason why we process and store in cache if (empty($remoteUrl)) { // No need to alarm. all good. If not URL just return. @@ -786,7 +876,18 @@ protected function getRemoteJsonData($remoteUrl, $options) { return NULL; } try { - $request = $this->httpClient->get($remoteUrl, $options); + if ($method == 'GET') { + $request = $this->httpClient->get($remoteUrl, $options); + } + elseif ($method == 'POST') { + $request = $this->httpClient->post($remoteUrl, $options); + } + elseif ($method == 'PUT') { + $request = $this->httpClient->put($remoteUrl, $options); + } + else { + return NULL; + } // Do not cache if things go bad. if ($request->getStatusCode() == '401') { $this->setNotAllowed(TRUE); @@ -819,7 +920,7 @@ protected function getRemoteJsonData($remoteUrl, $options) { catch (ServerException $exception) { $this->useCaches = FALSE; $responseMessage = $exception->getMessage(); - $this->loggerFactory->get('webform_strawberryfield') + $this->getLogger('webform_strawberryfield') ->error('We tried to contact @url but we could not.
The Remote server says: @response.
Check your query', [ '@url' => $remoteUrl, diff --git a/src/Element/WebformSnac.php b/src/Element/WebformSnac.php new file mode 100644 index 0000000..ea67079 --- /dev/null +++ b/src/Element/WebformSnac.php @@ -0,0 +1,114 @@ + 'Constellation', + '#rdftype' => 'thing' + ]; + return $info; + } + + /** + * {@inheritdoc} + */ + public static function getCompositeElements(array $element) { + + $elements = []; + $vocab = 'Constellation'; + $rdftype = 'thing'; + if (isset($element['#vocab'])) { + $vocab = $element['#vocab']; + } + if (($vocab == 'rdftype') && isset($element['#rdftype'])) { + $rdftype = trim($element['#rdftype']); + } + + $class = '\Drupal\webform_strawberryfield\Element\WebformLoC'; + $elements['label'] = [ + '#type' => 'textfield', + '#title' => t('Label'), + '#autocomplete_route_name' => 'webform_strawberryfield.auth_autocomplete', + '#autocomplete_route_parameters' => ['auth_type' => 'snac', 'vocab' => $vocab, 'rdftype'=> $rdftype ,'count' => 10], + '#attributes' => [ + 'data-source-strawberry-autocomplete-key' => 'label', + 'data-target-strawberry-autocomplete-key' => 'uri' + ], + + ]; + $elements['uri'] = [ + '#type' => 'url', + '#title' => t('Snac URL'), + '#attributes' => ['data-strawberry-autocomplete-value' => TRUE] + ]; + $elements['label']['#process'][] = [$class, 'processAutocomplete']; + return $elements; + } + + + + /** + * {@inheritdoc} + */ + public static function processWebformComposite(&$element, FormStateInterface $form_state, &$complete_form) { + // @Disclaimer: This function is the worst and deceiving. Keeping it here + // So i never make this error again. Because of + // \Drupal\webform\Plugin\WebformElement\WebformCompositeBase::prepareMultipleWrapper + // Basically, in case of having multiple elements :: processWebformComposite + // is *never* called because it actually converts the 'WebformComposite' element into a + // \Drupal\webform\Element\WebformMultiple calling ::processWebformMultiple element + // So basically whatever i do here gets skipped if multiple elements are allowed. + // Solution is acting here instead: + // \Drupal\webform_strawberryfield\Plugin\WebformElement\WebformLoC::prepareMultipleWrapper + $vocab = 'Constellation'; + $rdftype = 'thing'; + + $element = parent::processWebformComposite($element, $form_state, $complete_form); + if (isset($element['#vocab'])) { + $vocab = $element['#vocab']; + } + if (($vocab == 'rdftype') && isset($element['#rdftype'])) { + $rdftype = trim($element['#rdftype']); + } + $element['label']["#autocomplete_route_parameters"] = + ['auth_type' => 'snac', 'vocab' => $vocab, 'rdftype'=> $rdftype ,'count' => 10]; + + return $element; + } + + /** + * @param array $element + * @param \Drupal\Core\Form\FormStateInterface $form_state + * @param array $complete_form + * + * @return array + */ + public static function processAutocomplete(&$element, FormStateInterface $form_state, &$complete_form) { + $element = parent::processAutocomplete($element, $form_state, $complete_form); + $element['#attached']['library'][] = 'webform_strawberryfield/webform_strawberryfield.metadataauth.autocomplete'; + $element['#attached']['drupalSettings'] = [ + 'webform_strawberryfield_autocomplete' => [], + ]; + + $element['#attributes']['data-strawberry-autocomplete'] = 'Snac'; + return $element; + } + + +} diff --git a/src/Plugin/WebformElement/WebformSnac.php b/src/Plugin/WebformElement/WebformSnac.php new file mode 100644 index 0000000..dffe00d --- /dev/null +++ b/src/Plugin/WebformElement/WebformSnac.php @@ -0,0 +1,174 @@ + 'Constellation', + 'rdftype' => 'thing', + ] + parent::defineDefaultBaseProperties(); + } + + public function getDefaultProperties() { + $properties = parent::getDefaultProperties() + [ + 'vocab' => 'Constellation', + 'rdftype' => 'thing', + ]; + + return $properties; + } + + + + public function prepare( + array &$element, + WebformSubmissionInterface $webform_submission = NULL + ) { + + // @TODO explore this method to act on submitted data v/s element behavior + } + + /** + * Set multiple element wrapper. + * + * @param array $element + * An element. + */ + protected function prepareMultipleWrapper(array &$element) { + + parent::prepareMultipleWrapper($element); + + // Finally! + // This is the last chance we have to affect the render array + // This is where the original element type is also + // swapped by webform_multiple + // breaking all our #process callbacks. + $vocab = 'Constellation'; + $rdftype = 'thing'; + $vocab = $this->getElementProperty($element, 'vocab'); + $vocab = $vocab ?: $this->getDefaultProperty($vocab); + if ($vocab == 'rdftype') { + $rdftype = trim($this->getElementProperty($element, 'rdftype')); + } + + $rdftype = $rdftype ?: $this->getDefaultProperty($rdftype); + // This seems to have been an old Webform module variation + // Keeping it here until sure its not gone for good + if (isset($element['#element']['#webform_composite_elements']['label'])) { + $element['#element']['#webform_composite_elements']['label']["#autocomplete_route_parameters"] = + [ + 'auth_type' => 'snac', + 'vocab' => $vocab, + 'rdftype' => $rdftype, + 'count' => 10 + ]; + } + // For some reason i can not understand, when multiples are using + // Tables, the #webform_composite_elements -> 'label' is not used... + if (isset($element["#multiple__header"]) && $element["#multiple__header"] == true) { + $element['#element']['label']["#autocomplete_route_parameters"] = + [ + 'auth_type' => 'snac', + 'vocab' => $vocab, + 'rdftype' => $rdftype, + 'count' => 10 + ]; + } + } + + + /** + * {@inheritdoc} + */ + public function getPluginLabel() { + return $this->elementManager->isExcluded('webform_metadata_snac') ? $this->t('SNAC Constellation Terms') : parent::getPluginLabel(); + } + + /** + * {@inheritdoc} + */ + protected function formatHtmlItemValue(array $element, WebformSubmissionInterface $webform_submission, array $options = []) { + return $this->formatTextItemValue($element, $webform_submission, $options); + } + + /** + * {@inheritdoc} + */ + protected function formatTextItemValue(array $element, WebformSubmissionInterface $webform_submission, array $options = []) { + $value = $this->getValue($element, $webform_submission, $options); + + $lines = []; + if (!empty($value['uri'])) { + $lines[] = $value['uri']; + } + + if (!empty($value['label'])) { + $lines[] = $value['label']; + } + return $lines; + } + + /** + * {@inheritdoc} + */ + public function form(array $form, FormStateInterface $form_state) { + $form = parent::form($form, $form_state); + + $form['composite']['vocab'] = [ + '#type' => 'select', + '#options' => [ + 'Constellation' => 'All Constellation Entity Types', + 'rdftype' => 'By specific Constellation Entity Types', + ], + '#title' => $this->t("What SNAC query type to use."), + '#description' => $this->t('Specific Entity Types can be: person, corporateBody or family'), + ]; + // Not sure if this has a sub authority and how that works/if suggest + $form['composite']['rdftype'] = [ + '#type' => 'select', + '#options' => [ + 'person' => 'person', + 'corporateBody' => 'corporateBody', + 'family' => 'family', + ], + '#title' => $this->t("What SNAC Entity type to use as filter"), + '#description' => $this->t('Can be one of: person, corporateBody or family'), + '#default_value' => 'person', + '#states' => [ + 'visible' => [ + ':input[name="properties[vocab]"]' => ['value' => 'rdftype'], + ], + ], + ]; + + return $form; + } + +} From 75d98082f9c0fd4719acc43903a8f0e355f2dba6 Mon Sep 17 00:00:00 2001 From: Diego Pino Navarro Date: Tue, 17 Aug 2021 12:50:04 -0400 Subject: [PATCH 4/5] Fix lower case use statement --- src/Plugin/WebformHandler/strawberryFieldharvester.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Plugin/WebformHandler/strawberryFieldharvester.php b/src/Plugin/WebformHandler/strawberryFieldharvester.php index c890da2..f58002d 100644 --- a/src/Plugin/WebformHandler/strawberryFieldharvester.php +++ b/src/Plugin/WebformHandler/strawberryFieldharvester.php @@ -9,7 +9,7 @@ use Drupal\webform\Plugin\WebformElementEntityReferenceInterface; use Drupal\webform\Plugin\WebformHandlerBase; use Drupal\webform\WebformInterface; -use Drupal\webform\webformSubmissionInterface; +use Drupal\webform\WebformSubmissionInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Drupal\strawberryfield\Tools\Ocfl\OcflHelper; From 2c3aaee05b658c3d98d41a75f89a13493e601583 Mon Sep 17 00:00:00 2001 From: Diego Pino Navarro Date: Thu, 19 Aug 2021 13:54:33 -0400 Subject: [PATCH 5/5] adds Pubmed descriptor and term. Removed pair because it requires two entries. A top level descriptor and a sub heading match. A.g Descriptor: Virus Attachment qualifier: drug effects leads to this Descriptor Virus Attachment/drug effects IMPORTANT: autocomplete after the first entry works on full words. e.g Virus or Virus Attachment but not on Virus A --- .gitignore | 1 + src/Controller/AuthAutocompleteController.php | 119 +++++++++++-- src/Element/WebformMesh.php | 108 ++++++++++++ src/Plugin/WebformElement/WebformMesh.php | 162 ++++++++++++++++++ 4 files changed, 376 insertions(+), 14 deletions(-) create mode 100644 src/Element/WebformMesh.php create mode 100644 src/Plugin/WebformElement/WebformMesh.php diff --git a/.gitignore b/.gitignore index 78b63a5..140221c 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ src/Tools/Iiif/IiifHelper.php .idea/webform_strawberryfield.iml src/.DS_Store +.idea/runConfigurations/archipelago.xml diff --git a/src/Controller/AuthAutocompleteController.php b/src/Controller/AuthAutocompleteController.php index 48b0e36..7651ef3 100644 --- a/src/Controller/AuthAutocompleteController.php +++ b/src/Controller/AuthAutocompleteController.php @@ -177,11 +177,10 @@ public function handleAutocomplete(Request $request, $auth_type, $vocab = 'subje $cache_id = 'webform_strawberry:auth_lod:' . $cache_var; $cached = $this->cacheGet($cache_id); if ($cached) { - error_log('cached'); return new JsonResponse($cached->data); } - if ($this->currentUser->isAnonymous() && !$is_internal) { - sleep(5); + if ($this->currentUser->isAnonymous() && !$is_internal) { + sleep(1); } switch ($auth_type) { @@ -201,9 +200,15 @@ public function handleAutocomplete(Request $request, $auth_type, $vocab = 'subje case 'viaf': $results = $this->viaf($input); break; + case 'mesh': + $results = $this->mesh($input, $vocab, $rdftype); + break; + case 'snac': + $results = $this->snac($input, $vocab, $rdftype); + break; case 'europeana': if ($apikey) { - $results = $this->europeana($input, $vocab, $apikey); + $results = $this->europeana($input, $vocab, $apikey); } else { $this->messenger()->addError( @@ -211,10 +216,6 @@ public function handleAutocomplete(Request $request, $auth_type, $vocab = 'subje ) ); } - break; - case 'snac': - $results = $this->snac($input, $vocab, $rdftype); - break; } } // DO not cache NULL or FALSE. Those will be 401/403/500; @@ -281,7 +282,7 @@ protected function loc($input, $vocab, $rdftype) { ]; $path = $endpoint[$vocab]; - $input = urlencode($input); + $input = rawurlencode($input); if ($vocab == 'rdftype') { $urlindex = "/suggest/?q=" . $input . "&rdftype=" . $rdftype; @@ -335,7 +336,7 @@ protected function loc($input, $vocab, $rdftype) { * @return array */ protected function wikidata($input) { - $input = urlencode($input); + $input = rawurlencode($input); $urlindex = '&language=en&format=json&search=' . $input; $baseurl = 'https://www.wikidata.org/w/api.php?action=wbsearchentities'; $remoteUrl = $baseurl . $urlindex; @@ -610,7 +611,7 @@ protected function getty($input, $vocab = 'aat', $mode = 'fuzzy') { * @return array */ protected function viaf($input) { - $input = urlencode($input); + $input = rawurlencode($input); $urlindex = '&query=' . $input; $baseurl = 'https://viaf.org/viaf/AutoSuggest?'; $remoteUrl = $baseurl . $urlindex; @@ -625,7 +626,7 @@ protected function viaf($input) { if ($json_error == JSON_ERROR_NONE) { //WIKIdata will give is an success key will always return at least one, the query string if (count($jsondata) > 1) { - if (isset($jsondata['result']) && is_array($jsondata['result']) && count($jsondata['result']) >= 1) { + if (isset($jsondata['result']) && is_array($jsondata['result']) && count($jsondata['result']) >= 1) { foreach ($jsondata['result'] as $key => $item) { $desc = (isset($item['nametype'])) ? '(' . $item['nametype'] . ')' : NULL; $results[] = [ @@ -689,7 +690,7 @@ protected function europeana($input, $vocab, string $apikey) { return $results; } - $input = urlencode($input); + $input = rawurlencode($input); $urlindex = "/suggest?text=" . $input . "&type=" . $vocab ."&wskey=". $apikey ; @@ -737,7 +738,7 @@ protected function europeana($input, $vocab, string $apikey) { } if (($vocab == 'agent') && isset($result['dateOfBirth'])) { - $desc[] = $result['dateOfBirth'] . '/' . $result['dateOfDeath'] ?? '?'; + $desc[] = $result['dateOfBirth'] . '/' . $result['dateOfDeath'] ?? '?'; } $desc = !empty($desc) ? ' (' . implode(', ', $desc) . ')' : NULL; @@ -854,6 +855,96 @@ protected function snac($input, $vocab, $rdftype) { + /** + * @param $input + * The query + * @param $vocab + * The 'suggest' enabled endpoint at LoC + * + * @return array + */ + protected function mesh($input, $vocab, $rdftype) { + + //@TODO make the following allowed list a constant since we use it in + // \Drupal\webform_strawberryfield\Plugin\WebformElement\WebformMesh + if (!in_array($vocab, [ + 'descriptor', + 'term', + ])) { + // Drop before tryin to hit non existing vocab + $this->messenger()->addError( + $this->t('@vocab for MeSH autocomplete is not in in our allowed list.', + [ + '@vocab' => $vocab, + ] + ) + ); + $results[] = [ + 'value' => NULL, + 'label' => "Wrong Vocabulary {$vocab} in MeSH API Query", + ]; + return $results; + } + + // Here $rdftype acts as match + if (!in_array($rdftype, [ + 'startswith', + 'contains', + 'exact', + ])) { + // Drop before tryin to hit non existing vocab + $this->messenger()->addError( + $this->t('@rdftype Match type for MeSH autocomplete is not valid. It may be "exact","startswith" or "contains"', + [ + '@rdftype' => $rdftype, + ] + ) + ); + $results[] = [ + 'value' => NULL, + 'label' => "Wrong Match type for {$vocab} in MeSH API Query", + ]; + return $results; + } + + $input = rawurlencode($input); + $urlindex = "/mesh/lookup/{$vocab}?label=" . $input .'&limit=10&match=' . $rdftype; + $baseurl = 'https://id.nlm.nih.gov'; + $remoteUrl = $baseurl . $urlindex; + $options['headers'] = ['Accept' => 'application/json']; + $body = $this->getRemoteJsonData($remoteUrl, $options); + + $results = []; + $jsondata = json_decode($body, TRUE); + $json_error = json_last_error(); + if ($json_error == JSON_ERROR_NONE) { + if (count($jsondata) > 1) { + foreach ($jsondata as $entry) { + $results[] = [ + 'value' => $entry['resource'], + 'label' => $entry['label'], + ]; + } + } + else { + $results[] = [ + 'value' => NULL, + 'label' => "Sorry no match from MeSH for {$vocab}", + ]; + } + return $results; + } + $this->messenger()->addError( + $this->t('Looks like data fetched from @url is not in JSON format.
JSON says: @jsonerror
Please check your URL!', + [ + '@url' => $remoteUrl, + '@jsonerror' => $json_error, + ] + ) + ); + return []; + } + /** * @param $remoteUrl * @param $options diff --git a/src/Element/WebformMesh.php b/src/Element/WebformMesh.php new file mode 100644 index 0000000..d50cf13 --- /dev/null +++ b/src/Element/WebformMesh.php @@ -0,0 +1,108 @@ + 'descriptor', + '#matchtype' => 'startswith' + ]; + return $info; + } + + /** + * {@inheritdoc} + */ + public static function getCompositeElements(array $element) { + $elements = []; + $vocab = 'descriptor'; + $matchtype = 'startswith'; + if (isset($element['#vocab'])) { + $vocab = $element['#vocab']; + } + if (isset($element['#matchtype'])) { + $matchtype = $element['#matchtype']; + } + + $class = '\Drupal\webform_strawberryfield\Element\WebformMesh'; + $elements['label'] = [ + '#type' => 'textfield', + '#title' => t('MeSH @vocab Label',['@vocab' => $vocab]), + '#autocomplete_route_name' => 'webform_strawberryfield.auth_autocomplete', + '#autocomplete_route_parameters' => ['auth_type' => 'mesh', 'vocab' => $vocab, 'rdftype'=> $matchtype ,'count' => 10], + '#attributes' => [ + 'data-source-strawberry-autocomplete-key' => 'label', + 'data-target-strawberry-autocomplete-key' => 'uri' + ], + ]; + + $elements['uri'] = [ + '#type' => 'url', + '#title' => t('MeSH URL'), + '#attributes' => ['data-strawberry-autocomplete-value' => TRUE] + ]; + $elements['label']['#process'][] = [$class, 'processAutocomplete']; + return $elements; + } + + /** + * {@inheritdoc} + */ + public static function processWebformComposite(&$element, FormStateInterface $form_state, &$complete_form) { + // @Disclaimer: This function is the worst and deceiving. Keeping it here + // So i never make this error again. Because of + // \Drupal\webform\Plugin\WebformElement\WebformCompositeBase::prepareMultipleWrapper + // Basically, in case of having multiple elements :: processWebformComposite + // is *never* called because it actually converts the 'WebformComposite' element into a + // \Drupal\webform\Element\WebformMultiple calling ::processWebformMultiple element + + $vocab = 'descriptor'; + $matchtype = 'startswith'; + + $element = parent::processWebformComposite($element, $form_state, $complete_form); + if (isset($element['#vocab'])) { + $vocab = $element['#vocab']; + } + if (isset($element['#matchtype'])) { + $matchtype = $element['#matchtype']; + } + + $element['label']["#autocomplete_route_parameters"] = + ['auth_type' => 'mesh', 'vocab' => $vocab, 'rdftype'=> $matchtype ,'count' => 10]; + + return $element; + } + + /** + * @param array $element + * @param \Drupal\Core\Form\FormStateInterface $form_state + * @param array $complete_form + * + * @return array + */ + public static function processAutocomplete(&$element, FormStateInterface $form_state, &$complete_form) { + $element = parent::processAutocomplete($element, $form_state, $complete_form); + $element['#attached']['library'][] = 'webform_strawberryfield/webform_strawberryfield.metadataauth.autocomplete'; + $element['#attached']['drupalSettings'] = [ + 'webform_strawberryfield_autocomplete' => [], + ]; + + $element['#attributes']['data-strawberry-autocomplete'] = 'mesh'; + return $element; + } + +} diff --git a/src/Plugin/WebformElement/WebformMesh.php b/src/Plugin/WebformElement/WebformMesh.php new file mode 100644 index 0000000..fedf6d0 --- /dev/null +++ b/src/Plugin/WebformElement/WebformMesh.php @@ -0,0 +1,162 @@ + 'descriptor', + 'matchtype' => 'startswith', + ] + parent::defineDefaultBaseProperties(); + } + + public function getDefaultProperties() { + $properties = parent::getDefaultProperties() + [ + 'vocab' => 'descriptor', + 'matchtype' => 'startswith', + ]; + + return $properties; + } + + + + public function prepare( + array &$element, + WebformSubmissionInterface $webform_submission = NULL + ) { + + // @TODO explore this method to act on submitted data v/s element behavior + } + + /** + * Set multiple element wrapper. + * + * @param array $element + * An element. + */ + protected function prepareMultipleWrapper(array &$element) { + + parent::prepareMultipleWrapper($element); + + // Finally! + // This is the last chance we have to affect the render array + // This is where the original element type is also + // swapped by webform_multiple + // breaking all our #process callbacks. + $matchtype = trim($this->getElementProperty($element, 'matchtype')); + $matchtype = $matchtype?: $this->getDefaultProperty('matchtype'); + $vocab = $this->getElementProperty($element, 'vocab'); + $vocab = $vocab ?: $this->getDefaultProperty('vocab'); + if (isset($element['#element']['#webform_composite_elements']['label'])) { + $element['#element']['#webform_composite_elements']['label']["#autocomplete_route_parameters"] = + [ + 'auth_type' => 'mesh', + 'vocab' => $vocab, + 'rdftype' => $matchtype, + 'count' => 10 + ]; + } + // For some reason i can not understand, when multiples are using + // Tables, the #webform_composite_elements -> 'label' is not used... + if (isset($element["#multiple__header"]) && $element["#multiple__header"] == true) { + $element['#element']['label']["#autocomplete_route_parameters"] = + [ + 'auth_type' => 'mesh', + 'vocab' => $vocab, + 'rdftype' => $matchtype, + 'matchtype' => 'startswith', + 'count' => 10 + ]; + } + } + + + /** + * {@inheritdoc} + */ + public function getPluginLabel() { + return $this->elementManager->isExcluded('webform_metadata_mesh') ? $this->t('Medical Subject Heading MeSH') : parent::getPluginLabel(); + } + + /** + * {@inheritdoc} + */ + protected function formatHtmlItemValue(array $element, WebformSubmissionInterface $webform_submission, array $options = []) { + return $this->formatTextItemValue($element, $webform_submission, $options); + } + + /** + * {@inheritdoc} + */ + protected function formatTextItemValue(array $element, WebformSubmissionInterface $webform_submission, array $options = []) { + $value = $this->getValue($element, $webform_submission, $options); + + $lines = []; + if (!empty($value['uri'])) { + $lines[] = $value['uri']; + } + + if (!empty($value['label'])) { + $lines[] = $value['label']; + } + return $lines; + } + + /** + * {@inheritdoc} + */ + public function form(array $form, FormStateInterface $form_state) { + $form = parent::form($form, $form_state); + + $form['composite']['vocab'] = [ + '#type' => 'select', + '#options' => [ + 'descriptor' => 'Medical Subject Headings Descriptor (Subject Headings) API', + 'term' => 'Medical Subject Headings Term API', + ], + '#title' => $this->t("What MeSH Autocomplete API Type to use."), + '#description' => $this->t('See MeSH Subject HeadingsAPI'), + + ]; + $form['composite']['matchtype'] = [ + '#type' => 'select', + '#options' => [ + 'exact' => 'Exact, based on recommended Label. Will give you a single result or none.', + 'startswith' => 'Label Starts with.', + 'contains' => 'Label Contains.', + ], + '#title' => $this->t("What type of Match Query to perform"), + '#description' => $this->t('All match types return the same number of results. Exact matches only against the prefered label of the query.'), + ]; + return $form; + } + +}