diff --git a/wp-content/plugins/embl-taxonomy/README.md b/wp-content/plugins/embl-taxonomy/README.md index 7625a9f75..65483db30 100644 --- a/wp-content/plugins/embl-taxonomy/README.md +++ b/wp-content/plugins/embl-taxonomy/README.md @@ -115,13 +115,9 @@ A list of taxonomy terms can be viewed by visiting **Posts > EMBL Taxonomy**: /wp-admin/edit-tags.php?taxonomy=embl_taxonomy ``` -If the cached taxonomy is older than `MAX_AGE` an admin notice will appear linking to: +If the cached taxonomy is older than `MAX_AGE` an admin notice will appear with a button to resync the taxonomy. The button opens a modal that handles the sync process. Syncing is done in batches using mutliple API requests to avoid a timeout. -``` -/wp-admin/edit-tags.php?taxonomy=embl_taxonomy&sync=true -``` - -This URL will force a resync with the EMBL Taxonomy. +The `SYNC_MAX_TERMS` constant in [register.php](/wp-content/plugins/embl-taxonomy/includes/register.php) defines the batch size. Lowering this value will slow down the process by using shorter requests. ## ACF Configuration diff --git a/wp-content/plugins/embl-taxonomy/acf-json/embl-taxonomy-terms.json b/wp-content/plugins/embl-taxonomy/acf-json/embl-taxonomy-terms.json index 8abf55579..c32a93875 100644 --- a/wp-content/plugins/embl-taxonomy/acf-json/embl-taxonomy-terms.json +++ b/wp-content/plugins/embl-taxonomy/acf-json/embl-taxonomy-terms.json @@ -6,67 +6,76 @@ "key": "field_embl_taxonomy_term_who", "label": "Who", "name": "embl_taxonomy_term_who", + "aria-label": "", "type": "taxonomy", "instructions": "", "required": 0, "conditional_logic": 0, "wrapper": { - "width": "33", + "width": "", "class": "", "id": "" }, "taxonomy": "embl_taxonomy", - "field_type": "select", - "allow_null": 1, "add_term": 0, "save_terms": 0, "load_terms": 0, "return_format": "id", - "multiple": 0 + "field_type": "select", + "allow_null": 1, + "bidirectional": 0, + "multiple": 0, + "bidirectional_target": [] }, { "key": "field_embl_taxonomy_term_what", "label": "What", "name": "embl_taxonomy_term_what", + "aria-label": "", "type": "taxonomy", "instructions": "", "required": 0, "conditional_logic": 0, "wrapper": { - "width": "33", + "width": "", "class": "", "id": "" }, "taxonomy": "embl_taxonomy", - "field_type": "select", - "allow_null": 1, "add_term": 0, "save_terms": 0, "load_terms": 0, "return_format": "id", - "multiple": 0 + "field_type": "select", + "allow_null": 1, + "bidirectional": 0, + "multiple": 0, + "bidirectional_target": [] }, { "key": "field_embl_taxonomy_term_where", "label": "Where", "name": "embl_taxonomy_term_where", + "aria-label": "", "type": "taxonomy", "instructions": "", "required": 0, "conditional_logic": 0, "wrapper": { - "width": "33", + "width": "", "class": "", "id": "" }, "taxonomy": "embl_taxonomy", - "field_type": "select", - "allow_null": 1, "add_term": 0, "save_terms": 0, "load_terms": 0, "return_format": "id", - "multiple": 0 + "field_type": "select", + "allow_null": 1, + "bidirectional": 0, + "multiple": 0, + "bidirectional_target": [] } ], "location": [ @@ -93,5 +102,6 @@ "hide_on_screen": "", "active": true, "description": "", - "modified": 1636713925 + "show_in_rest": 0, + "modified": 1697465104 } diff --git a/wp-content/plugins/embl-taxonomy/assets/embl-taxonomy.js b/wp-content/plugins/embl-taxonomy/assets/embl-taxonomy.js new file mode 100644 index 000000000..7bbf91a0d --- /dev/null +++ b/wp-content/plugins/embl-taxonomy/assets/embl-taxonomy.js @@ -0,0 +1,96 @@ +(() => { + // Bail if no sync button + $button = document.querySelector('#embl-taxonomy-sync'); + if (!$button) return; + + // Append styles for modal + const $style = document.createElement('style'); + $style.id = 'embl-taxonomy-css'; + $style.innerHTML = ` +#embl-taxonomy-modal { + border: 0px; + border-radius: 5px; + box-shadow: + 0px 0.3px 1.4px rgba(0, 0, 0, 0.056), + 0px 0.7px 3.3px rgba(0, 0, 0, 0.081), + 0px 1.3px 6.3px rgba(0, 0, 0, 0.1), + 0px 2.2px 11.2px rgba(0, 0, 0, 0.119), + 0px 4.2px 20.9px rgba(0, 0, 0, 0.144), + 0px 10px 50px rgba(0, 0, 0, 0.2) + ; + padding: 10px 20px; + text-align: center; +} +#embl-taxonomy-modal::backdrop { + background-color: rgb(0,0,0,0.2); + backdrop-filter: blur(2px); +} +#embl-taxonomy-modal .spinner { + float: none; + vertical-align: top; + margin: 0 10px 0 0; +} +`; + document.head.appendChild($style); + + // Get localized data from PHP + const {data, path, token, redirect} = window.emblTaxonomySettings; + + // Create modal for output + const $modal = document.createElement('dialog'); + $modal.id = 'embl-taxonomy-modal'; + $modal.style.display = 'none'; + $modal.innerHTML = ` +
${data.reload}
+ +`; + document.body.appendChild($modal); + const $progress = $modal.querySelector('progress'); + + // Sync button handler + const startSync = async (url) => { + try { + const response = await fetch(url, { + method: 'POST', + headers: { + 'X-WP-Nonce': token + } + }); + const json = await response.json(); + if (json.error) { + $modal.innerHTML = `${json.error}
`; + return; + } + if (Object.hasOwn(json, 'total')) { + $progress.max = json.total; + } + if (Object.hasOwn(json, 'offset')) { + $progress.value = json.offset; + } + if (json.success === true) { + $progress.value = $progress.max; + window.location.href = redirect; + return; + } + if (json.next) { + startSync(new URL(json.next)); + return; + } + throw new Error(); + } catch { + $modal.innerHTML = `${data.error}
`; + } + }; + + $button.addEventListener('click', (ev) => { + ev.preventDefault(); + $button.disabled = true; + $button.innerText = 'Syncing...'; + $progress.removeAttribute('max'); + $progress.removeAttribute('value'); + $modal.style.removeProperty('display'); + $modal.showModal(); + startSync(new URL(path)); + }); +})(); diff --git a/wp-content/plugins/embl-taxonomy/includes/register.php b/wp-content/plugins/embl-taxonomy/includes/register.php index 51eb8d258..22acc2cb7 100644 --- a/wp-content/plugins/embl-taxonomy/includes/register.php +++ b/wp-content/plugins/embl-taxonomy/includes/register.php @@ -4,8 +4,13 @@ if ( ! class_exists('EMBL_Taxonomy_Register') ) : + class EMBL_Taxonomy_Register { + // Maximum number of terms to sync per API request + // Set as high as possible but reduce if requests timeout + const SYNC_MAX_TERMS = 500; + // Options table keys const OPTION_MODIFIED = EMBL_Taxonomy::TAXONOMY_NAME . '_modified'; @@ -14,8 +19,6 @@ class EMBL_Taxonomy_Register { protected $labels; - private $sync_error = false; - public function __construct() { $this->labels = array( @@ -33,7 +36,6 @@ public function __construct() { add_filter(EMBL_Taxonomy::TAXONOMY_NAME . '_name', array($this, 'filter_term_name'), 10, 3); if (is_admin()) { - add_action('admin_init', array($this, 'action_admin_init')); add_action('admin_notices', array($this, 'action_admin_notices')); add_action('admin_enqueue_scripts', array($this, 'action_admin_enqueue')); } @@ -48,12 +50,8 @@ public function __construct() { */ add_filter( 'manage_edit-embl_taxonomy_columns' , 'wptc_embl_taxonomy_columns' ); function wptc_embl_taxonomy_columns( $columns ) { - // remove slug column - // unset($columns['slug']); - - // add column - $columns['embl_taxonomy_term_uuid'] = __('EMBL Term UUID'); - return $columns; + $columns['embl_taxonomy_term_uuid'] = __('EMBL Term UUID', 'embl'); + return $columns; } /* @@ -63,18 +61,18 @@ function wptc_embl_taxonomy_columns( $columns ) { */ add_filter( 'manage_embl_taxonomy_custom_column', 'wptc_embl_taxonomy_column_content', 10, 3 ); function wptc_embl_taxonomy_column_content( $content, $column_name, $term_id ) { - // get the term object - $term = get_term( $term_id, 'embl_taxonomy' ); - // check if column is our custom column - if ( 'embl_taxonomy_term_uuid' == $column_name ) { + // get the term object + $term = get_term( $term_id, 'embl_taxonomy' ); + // check if column is our custom column + if ( 'embl_taxonomy_term_uuid' == $column_name ) { $full_term = embl_taxonomy_get_term($term->term_id); // Eventually we should link back to the contenHub, however we don't currently // have a good way to search by UUID // https://dev.content.embl.org/api/v1/pattern.html?filter-content-type=profiles&filter-uuid=2a270b68-46c3-4b3f-92c5-0a65eb896c86&pattern=node-display-title // the above query depends on knowing the conent type - $content = ''.end($full_term->meta['embl_taxonomy_ids']).'
';
- }
- return $content;
+ $content = ''.end($full_term->meta['embl_taxonomy_ids']).'
';
+ }
+ return $content;
}
$this->set_read_only();
@@ -144,9 +142,21 @@ public function register_taxonomy() {
/**
* Hook for `rest_api_init`
- * Add taxonomy terms assigned to posts
*/
public function register_rest_api() {
+ // Using `POST` because GET requests are easier to accidentally invoke
+ // API route will be called multiple times with the `offset` parameter
+ register_rest_route(EMBL_Taxonomy::TAXONOMY_NAME . '/v1', '/sync/', array(
+ 'methods' => 'POST',
+ 'callback' => array($this, 'sync_taxonomy'),
+ // Secure the API route
+ // Header token from `wp_localize_script` below is required
+ 'permission_callback' => function () {
+ return current_user_can('manage_categories');
+ }
+ ));
+
+ // Add taxonomy terms assigned to posts as field for WP REST API
register_rest_field(
EMBL_Taxonomy::TAXONOMY_TYPES,
EMBL_Taxonomy::TAXONOMY_NAME . '_terms',
@@ -208,59 +218,87 @@ static public function get_wp_taxonomy() {
/**
* Read and parse the EMBL Taxonomy API as JSON
- * Error messages are picked up by the `admin_notices` action
* The WordPress Taxonomy is rebuilt with matching ID terms synced
*/
- public function sync_taxonomy() {
- // Make contenthub taxonomy api call to work on local
- $whitelist = array(
- '127.0.0.1',
- '::1'
- );
+ public function sync_taxonomy(WP_REST_Request $request = null) {
- if(in_array($_SERVER['REMOTE_ADDR'], $whitelist)) {
- $embl_taxonomy_url = "https://content.embl.org/api/v1/pattern.json?pattern=embl-ontology&source=contenthub";
- }
- else {
- // Prod Proxy endpoint.
- $embl_taxonomy_url = embl_taxonomy_get_url();
+ $params = array();
+ if ($request) {
+ $params = $request->get_query_params();
}
+ $offset = isset($params['offset']) ? intval($params['offset']) : -1;
- // Attempt to read the EMBL Taxonomy API
- $data = file_get_contents($embl_taxonomy_url);
- if ($data === false) {
- $this->sync_error = sprintf(
- __('The %1$s API endpoint could not be accessed.', 'embl'),
- $this->labels['name']
- );
- return $this->sync_error;
- }
+ // Temporary file location to cache old and new data during sync process
+ $oldpath = trailingslashit( WP_CONTENT_DIR ) . 'uploads/' . EMBL_Taxonomy::TAXONOMY_NAME . '.old.txt';
+ $newpath = trailingslashit( WP_CONTENT_DIR ) . 'uploads/' . EMBL_Taxonomy::TAXONOMY_NAME . '.new.txt';
- // Attempt to parse API results
- $json_terms = self::decode_terms($data);
- if (empty($json_terms)) {
- $this->sync_error = sprintf(
- __('The %1$s API result could not be parsed.', 'embl'),
- $this->labels['name']
- );
- return $this->sync_error;
+ // Start new sync process if not offset is provided
+ if ($offset < 0) {
+ // Delete temporary files
+ if (file_exists($oldpath)) {
+ unlink($oldpath);
+ }
+ if (file_exists($newpath)) {
+ unlink($newpath);
+ }
+ // Get the existing WordPress taxonomy
+ file_put_contents($oldpath, serialize(self::get_wp_taxonomy()));
+ // Fetch the new terms
+ $value = $this->sync_taxonomy_temp_file($newpath);
+ // Handle errors
+ if (is_string($value)) {
+ if ($request) {
+ return array(
+ 'error' => $value
+ );
+ } else {
+ throw new Exception($value);
+ }
+ }
+ // Return the URL for the first batch
+ if ($request) {
+ return array(
+ 'next' => rest_url(EMBL_Taxonomy::TAXONOMY_NAME .'/v1/sync/?offset=0'),
+ 'total' => $value,
+ 'offset' => 0
+ );
+ exit;
+ }
}
- // Generate the new taxonomy terms from the API terms provided
- $new_terms = array();
-
- self::generate_terms($json_terms, $new_terms);
-
- self::sort_terms($new_terms);
-
- // Get the existing WordPress taxonomy
- $wp_taxonomy = self::get_wp_taxonomy();
+ // Retrieve the old terms
+ $wp_taxonomy = unserialize(file_get_contents($oldpath));
+ // Retrieve the new terms
+ $new_terms = unserialize(file_get_contents($newpath));
// Allow taxonomy terms to be inserted
$this->set_read_only(false);
+ // Batch size for Request API or sync all terms (for WP CLI)
+ $max_terms = $request ? self::SYNC_MAX_TERMS : PHP_INT_MAX;
+
// Iterate over all terms to sync
+ $i = 0;
foreach ($new_terms as $term) {
+ // Skip terms before the offset
+ if ($i < $offset) {
+ $i++;
+ continue;
+ }
+ // Check if batch is complete
+ if ($i >= $offset + $max_terms) {
+ $this->set_read_only(true);
+ // Cache progress and redirect to continue
+ file_put_contents($oldpath, serialize($wp_taxonomy));
+ // Return the URL for the next batch
+ return array(
+ 'next' => rest_url(EMBL_Taxonomy::TAXONOMY_NAME . '/v1/sync/?offset=' . $i),
+ 'total' => count($new_terms),
+ 'offset' => $i,
+ );
+ exit;
+ }
+ $i++;
// Find existing `WP_Term` in the taxonomy based on META_IDS structure
$wp_term = null;
@@ -339,7 +377,61 @@ public function sync_taxonomy() {
update_option(self::OPTION_MODIFIED, time());
- return true;
+ unlink($newpath);
+ unlink($oldpath);
+
+ // Return success message
+ return array(
+ 'success' => true,
+ 'terms' => count($new_terms)
+ );
+ }
+
+ /**
+ * Request new taxonomy terms from the EMBL Taxonomy API
+ * This is done at the start of the sync process
+ */
+ private function sync_taxonomy_temp_file($newpath) {
+ // Make contenthub taxonomy api call to work on local
+ $whitelist = array(
+ '127.0.0.1',
+ '::1'
+ );
+ if(in_array($_SERVER['REMOTE_ADDR'], $whitelist)) {
+ $embl_taxonomy_url = "https://content.embl.org/api/v1/pattern.json?pattern=embl-ontology&source=contenthub";
+ } else {
+ // Prod Proxy endpoint.
+ $embl_taxonomy_url = embl_taxonomy_get_url();
+ }
+
+ // Attempt to read the EMBL Taxonomy API
+ $data = file_get_contents($embl_taxonomy_url);
+ if ($data === false) {
+ return sprintf(
+ __('The %1$s API endpoint could not be accessed.', 'embl'),
+ $this->labels['name']
+ );
+ }
+
+ // Attempt to parse API results
+ $json_terms = self::decode_terms($data);
+ if (empty($json_terms)) {
+ return sprintf(
+ __('The %1$s API result could not be parsed.', 'embl'),
+ $this->labels['name']
+ );
+ }
+
+ // Generate the new taxonomy terms from the API terms provided
+ $new_terms = array();
+
+ self::generate_terms($json_terms, $new_terms);
+
+ self::sort_terms($new_terms);
+
+ file_put_contents($newpath, serialize($new_terms));
+
+ return count($new_terms);
}
/**
@@ -457,46 +549,19 @@ static private function sort_terms(& $terms) {
$blen = count($b[EMBL_Taxonomy::META_IDS]);
// Sort alphabetically if same level
if ($a[EMBL_Taxonomy::META_IDS][0] !== $b[EMBL_Taxonomy::META_IDS][0] || $alen === $blen) {
- return $a['name'] > $b['name'];
+ return strcmp($a['name'], $b['name']);
}
// Sort by longest chain
- return $alen > $blen;
+ return $alen - $blen;
});
}
- /**
- * Action `admin_init`
- */
- public function action_admin_init() {
- if ( ! current_user_can('manage_categories')) {
- return;
- }
- global $pagenow;
- if ($pagenow !== 'edit-tags.php') {
- return;
- }
- if (array_key_exists('taxonomy', $_GET) && $_GET['taxonomy'] !== EMBL_Taxonomy::TAXONOMY_NAME) {
- return;
- }
- if (array_key_exists('sync', $_GET) && $_GET['sync'] === 'true') {
- $this->sync_taxonomy();
- }
- }
-
/**
* Action `admin_notices`
* Show admin warning notice if the taxonomy needs syncing
* Show admin success notice if a sync happening recently
*/
public function action_admin_notices() {
-
- if ($this->sync_error) {
- printf('%2$s
%2$s %3$s
%2$s %3$s
%2$s %3$s
%2$s
%2$s %3$s