From b327ed5184ba781159cd4fcead83557d29f15201 Mon Sep 17 00:00:00 2001 From: Nathanael Dewhurst Date: Tue, 10 Feb 2015 17:00:35 -0500 Subject: [PATCH 1/8] Basic functional/performance revisions - Refactoring _parsely_get_tags for performance (particularly when large vocabs are involved) and simplicity. - Tweaking syntax in parsely_should_add_metas to avoid fatal error in some PHP versions. --- parsely.module | 34 +++++++++++++--------------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/parsely.module b/parsely.module index 56e798b..85e9a9b 100644 --- a/parsely.module +++ b/parsely.module @@ -68,16 +68,17 @@ function parsely_permission() { * are authenticated here. */ function parsely_should_add_metas() { + // @todo: This needs to work on non-node pages. $node = menu_get_object(); if ($node === NULL) { return FALSE; } - if (empty(variable_get('parsely_apikey', ''))) { + $parsely_api_key = variable_get('parsely_apikey', ''); + if (empty($parsely_api_key)) { return FALSE; } - $path = drupal_strtolower(drupal_get_path_alias($_GET['q'])); if (drupal_match_path($path, implode("\r", parsely_paths_to_ignore()))) { return FALSE; @@ -212,31 +213,22 @@ function _parsely_get_section($node) { function _parsely_get_tags($node) { $supported_vocabularies = variable_get('parsely_tag_vocabularies'); - if (!module_exists('taxonomy') || $supported_vocabularies === NULL) + if (!module_exists('taxonomy') || $supported_vocabularies === NULL) { return array(); - - // Get a mapping of all the terms in this vocabulary so that we can look up - // the name of the active terms by term id - $_terms = array(); - foreach ($supported_vocabularies as $vid) { - $_terms = array_merge($_terms, taxonomy_get_tree($vid)); - } - $terms = array(); - foreach ($_terms as $term) { - $terms[$term->tid] = $term->name; } - // Check what vocabulary terms are on the page that is loading - $active_terms_query = db_select('taxonomy_index', 't') - ->fields('t') - ->condition('nid', $node->nid, '=') + // Fetch the names of all terms that (a) belong to one of the supported + // vocabularies and (b) are associated with the given node. + $active_terms_query = db_select('taxonomy_index', 'ti'); + $active_terms_query->join('taxonomy_term_data', 'ttd', 'ti.tid=ttd.tid'); + $active_terms_result = $active_terms_query->fields('ttd', array('name')) + ->condition('ti.nid', $node->nid) + ->condition('ttd.vid', $supported_vocabularies, 'IN') ->execute(); - // Build up and return active_terms $active_terms = array(); - while ($row = $active_terms_query->fetchAssoc()) { - if (isset($row['tid'])) - array_push($active_terms, $terms[$row['tid']]); + if ($active_terms_result->rowCount() > 0) { + $active_terms = $active_terms_result->fetchCol(); } return $active_terms; From 7d044969038aced6e64dd53b20c0ac3628f250d8 Mon Sep 17 00:00:00 2001 From: Nathanael Dewhurst Date: Tue, 10 Feb 2015 17:34:20 -0500 Subject: [PATCH 2/8] Fixing _parsely_get_section to use vocab in query, etc. --- parsely.module | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/parsely.module b/parsely.module index 85e9a9b..984e6f4 100644 --- a/parsely.module +++ b/parsely.module @@ -194,19 +194,26 @@ function _parsely_get_pub_date_utc($node) { */ function _parsely_get_section($node) { $section_vocabulary = variable_get('parsely_section_vocabulary'); - if (!module_exists('taxonomy') || $section_vocabulary === NULL) + if (!module_exists('taxonomy') || $section_vocabulary === NULL) { return 'Uncategorized'; + } - $active_terms_query = db_select('taxonomy_index', 't') - ->fields('t') - ->condition('nid', $node->nid, '=') + // Fetch the name of the first term from the given vocabulary that is + // associated with the given node. + $section_term_query = db_select('taxonomy_index', 'ti'); + $section_term_query->join('taxonomy_term_data', 'ttd', 'ti.tid=ttd.tid'); + $section_term_result = $section_term_query->fields('ttd', array('name')) + ->condition('ti.nid', $node->nid) + ->condition('ttd.vid', $section_vocabulary) + ->orderBy('ti.created') + ->range(0, 1) ->execute(); - $row = $active_terms_query->fetchAssoc(); - if (isset($row['tid'])) { - $term = taxonomy_term_load($row['tid']); - return $term->name; - } else { + if ($section_term_result->rowCount() > 0) { + $section_term_name = $section_term_result->fetchField(); + return $section_term_name; + } + else { return 'Uncategorized'; } } From 8e7809623da5863f9f4f215a8075679b37bb95ef Mon Sep 17 00:00:00 2001 From: Nathanael Dewhurst Date: Tue, 10 Feb 2015 19:41:28 -0500 Subject: [PATCH 3/8] Restructuring section assignment to allow alteration --- parsely.module | 41 ++++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/parsely.module b/parsely.module index 984e6f4..070bdd1 100644 --- a/parsely.module +++ b/parsely.module @@ -193,29 +193,31 @@ function _parsely_get_pub_date_utc($node) { * for sections. */ function _parsely_get_section($node) { + $section_name = 'Uncategorized'; $section_vocabulary = variable_get('parsely_section_vocabulary'); - if (!module_exists('taxonomy') || $section_vocabulary === NULL) { - return 'Uncategorized'; + + if (module_exists('taxonomy') && !is_null($section_vocabulary)) { + // Fetch the name of the first term from the given vocabulary that is + // associated with the given node. + $section_term_query = db_select('taxonomy_index', 'ti'); + $section_term_query->join('taxonomy_term_data', 'ttd', 'ti.tid=ttd.tid'); + $section_term_result = $section_term_query->fields('ttd', array('name')) + ->condition('ti.nid', $node->nid) + ->condition('ttd.vid', $section_vocabulary) + ->orderBy('ti.created') + ->range(0, 1) + ->execute(); + + if ($section_term_result->rowCount() > 0) { + $section_name = $section_term_result->fetchField(); + } } - // Fetch the name of the first term from the given vocabulary that is - // associated with the given node. - $section_term_query = db_select('taxonomy_index', 'ti'); - $section_term_query->join('taxonomy_term_data', 'ttd', 'ti.tid=ttd.tid'); - $section_term_result = $section_term_query->fields('ttd', array('name')) - ->condition('ti.nid', $node->nid) - ->condition('ttd.vid', $section_vocabulary) - ->orderBy('ti.created') - ->range(0, 1) - ->execute(); + // Allow any module to alter the section as it sees fit. + // @todo: Consider passing the section term as well, if applicable. + drupal_alter('parsely_section', $section_name, $node, $section_vocabulary); - if ($section_term_result->rowCount() > 0) { - $section_term_name = $section_term_result->fetchField(); - return $section_term_name; - } - else { - return 'Uncategorized'; - } + return $section_name; } function _parsely_get_tags($node) { @@ -244,6 +246,7 @@ function _parsely_get_tags($node) { function _parsely_get_node_metadata() { $node = menu_get_object(); + // @todo: Integrate with metatag module and/or support tokens, etc. $parsely_meta = array( '@context' => 'http://schema.org', '@type' => 'WebPage', From 4a380332e9e9f60109d59950bb30913be5a346ea Mon Sep 17 00:00:00 2001 From: Nathanael Dewhurst Date: Thu, 12 Feb 2015 12:39:43 -0500 Subject: [PATCH 4/8] Adding admin option to help guide section term selection --- parsely.admin.inc | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/parsely.admin.inc b/parsely.admin.inc index 38f15c7..30c7731 100644 --- a/parsely.admin.inc +++ b/parsely.admin.inc @@ -19,13 +19,36 @@ function parsely_admin_settings($form, &$form_state) { ); if (module_exists('taxonomy')) { - $form['parsely_section_vocabulary'] = array( + $form['parsely_sections_wrapper'] = array( + '#type' => 'fieldset', + '#title' => t('Section Settings'), + '#collapsible' => FALSE, + '#collapsed' => FALSE, + ); + $form['parsely_sections_wrapper']['parsely_section_vocabulary'] = array( '#type' => 'radios', '#title' => t('Section Vocabulary'), '#options' => _parsely_vocab_array_format(taxonomy_get_vocabularies()), - '#description' => t('Select the taxonomy to use for Parse.ly sections. The first term in this vocabulary is used to populate the Parse.ly section variable.'), + '#description' => t('Select the taxonomy to use for Parse.ly sections. By default, the first matching term in this vocabulary for a given page is used to populate the Parse.ly section variable.'), '#default_value' => variable_get('parsely_section_vocabulary', ''), ); + $form['parsely_sections_wrapper']['parsely_section_term_criteria'] = array( + '#title' => t('Section term selection criteria'), + '#type' => 'radios', + '#options' => array( + 'first' => t('First term'), + 'last' => t('Last term'), + 'highest' => t('Highest-level term'), + 'ancestor' => t('Highest-level ancestor'), + ), + // Provide a long-winded explanation of how this impacts term selection. + // Note that there's currently no way to say "find the last/newest term, + // and then select its highest-level ancestor" or "use the term that + // appears first in the set of terms for a given term/entity reference + // field," etc. + '#description' => t('If a given node/page may be associated with multiple terms from the vocabulary you selected above, this setting can help determine which term to use. "First term" and "Last term" will choose a term based on the time at which they were associated with the given node (rather than the order in which they appear within a term reference field). "Highest term" will choose the term that lives closest to the root of the vocabulary (defaulting to the "first"/oldest of such terms if more than one exist). For hierarchical vocabularies, you can also opt to use the "Highest-level ancestor" of a given term, which means that if a given node is tagged with a lower-lever term (e.g. corresponding to a subsection of your site), we will report that term\'s parent (or grandparent, etc.) term as the section. Wherever a choice must be made between terms at the same depth, the first term will be chosen.'), + '#default_value' => variable_get('parsely_section_term_criteria', 'first'), + ); $form['parsely_tag_vocabularies'] = array( '#type' => 'checkboxes', From bbf872280c7389df13ca90527272aae1e6169c5a Mon Sep 17 00:00:00 2001 From: Nathanael Dewhurst Date: Thu, 12 Feb 2015 17:34:01 -0500 Subject: [PATCH 5/8] New options for controlling which term is used to designate parse.ly section. Admins can now specify criteria for choosing between multiple terms in the given vocab, and can elect to always use top-level terms in hierarchical vocabs. --- parsely.admin.inc | 10 +++--- parsely.module | 90 ++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 78 insertions(+), 22 deletions(-) diff --git a/parsely.admin.inc b/parsely.admin.inc index 30c7731..5e675eb 100644 --- a/parsely.admin.inc +++ b/parsely.admin.inc @@ -29,11 +29,11 @@ function parsely_admin_settings($form, &$form_state) { '#type' => 'radios', '#title' => t('Section Vocabulary'), '#options' => _parsely_vocab_array_format(taxonomy_get_vocabularies()), - '#description' => t('Select the taxonomy to use for Parse.ly sections. By default, the first matching term in this vocabulary for a given page is used to populate the Parse.ly section variable.'), + '#description' => t('Select the taxonomy vocabulary to use for Parse.ly sections. A single term from this vocabulary will be chosen for each tracked node, where applicable, using the criterion specified below.'), '#default_value' => variable_get('parsely_section_vocabulary', ''), ); - $form['parsely_sections_wrapper']['parsely_section_term_criteria'] = array( - '#title' => t('Section term selection criteria'), + $form['parsely_sections_wrapper']['parsely_section_term_criterion'] = array( + '#title' => t('Section Term Selection Criteria'), '#type' => 'radios', '#options' => array( 'first' => t('First term'), @@ -46,8 +46,8 @@ function parsely_admin_settings($form, &$form_state) { // and then select its highest-level ancestor" or "use the term that // appears first in the set of terms for a given term/entity reference // field," etc. - '#description' => t('If a given node/page may be associated with multiple terms from the vocabulary you selected above, this setting can help determine which term to use. "First term" and "Last term" will choose a term based on the time at which they were associated with the given node (rather than the order in which they appear within a term reference field). "Highest term" will choose the term that lives closest to the root of the vocabulary (defaulting to the "first"/oldest of such terms if more than one exist). For hierarchical vocabularies, you can also opt to use the "Highest-level ancestor" of a given term, which means that if a given node is tagged with a lower-lever term (e.g. corresponding to a subsection of your site), we will report that term\'s parent (or grandparent, etc.) term as the section. Wherever a choice must be made between terms at the same depth, the first term will be chosen.'), - '#default_value' => variable_get('parsely_section_term_criteria', 'first'), + '#description' => t('If a given node may be associated with multiple terms from the vocabulary you selected above, this setting can help determine which term to use. "First term" and "Last term" will choose a term based on the date/time the term was created (this may differ from the relative position of a term within a term reference field). "Highest term" will choose the term that lives closest to the root of the vocabulary (defaulting to the "first"/oldest of such terms if more than one exist). For hierarchical vocabularies, you can also opt to use the "Highest-level ancestor" of a given term, which means that if a given node is tagged with a lower-lever term (e.g. corresponding to a subsection of your site), we will report that term\'s parent (or grandparent, etc.) term as the section. Wherever a choice must be made between terms at the same depth, the first term will be chosen.'), + '#default_value' => variable_get('parsely_section_term_criterion', 'first'), ); $form['parsely_tag_vocabularies'] = array( diff --git a/parsely.module b/parsely.module index 070bdd1..ab9cc5f 100644 --- a/parsely.module +++ b/parsely.module @@ -189,33 +189,89 @@ function _parsely_get_pub_date_utc($node) { } /** - * Section is the first active term from the vocabulary we've been told to use - * for sections. + * Attempt to determine the "section" to which a given node belongs. */ function _parsely_get_section($node) { $section_name = 'Uncategorized'; - $section_vocabulary = variable_get('parsely_section_vocabulary'); + $section_term = NULL; + $section_vocabulary_id = variable_get('parsely_section_vocabulary'); + + if (module_exists('taxonomy') && !is_null($section_vocabulary_id)) { + // Find the most relevant term from the designated vocab, using the + // criterion specified in the module configuration. See + // parsely_admin_settings() for an explanation of the supported criteria. + $selection_criterion = variable_get('parsely_section_term_criterion', 'first'); - if (module_exists('taxonomy') && !is_null($section_vocabulary)) { - // Fetch the name of the first term from the given vocabulary that is - // associated with the given node. $section_term_query = db_select('taxonomy_index', 'ti'); $section_term_query->join('taxonomy_term_data', 'ttd', 'ti.tid=ttd.tid'); - $section_term_result = $section_term_query->fields('ttd', array('name')) + $section_term_query->fields('ttd', array('tid')) ->condition('ti.nid', $node->nid) - ->condition('ttd.vid', $section_vocabulary) - ->orderBy('ti.created') - ->range(0, 1) - ->execute(); - - if ($section_term_result->rowCount() > 0) { - $section_name = $section_term_result->fetchField(); + ->condition('ttd.vid', $section_vocabulary_id); + + // "First term" or "Last term." + if ($selection_criterion == 'first' || $selection_criterion == 'last') { + $sort_direction = ($selection_criterion == 'first') ? 'ASC' : 'DESC'; + $section_term_query->orderBy('ttd.tid', $sort_direction) + ->range(0, 1); + $section_term_result = $section_term_query->execute(); + + if ($section_term_result->rowCount() > 0) { + $section_tid = $section_term_result->fetchField(); + $section_term = taxonomy_term_load($section_tid); + } + } + // "Highest-level term." + elseif ($selection_criterion == 'highest') { + $all_terms_result = $section_term_query->orderBy('ttd.tid') + ->execute(); + + if ($all_terms_result->rowCount() > 0) { + $tids = $all_terms_result->fetchCol(); + $terms = taxonomy_term_load_multiple($tids); + // Examine each term and see how many parents it has. We could use + // taxonomy_get_tree() to get the depths of every term in the vocab, + // but that could be problematic for large vocabs. + foreach ($terms as $term) { + // Calculate depth by repeatedly calling taxonomy_get_parents(). This + // is preferable to calling taxonomy_get_parents_all(), because that + // would include all parents of any multi-parent terms, skewing our + // generation count. Note that $depth will be "1" for a top-level term + // here instead of the conventional "0." + $t = $term; + $depth = 0; + while (!is_null($t)) { + $depth++; + $parents = taxonomy_get_parents($t->tid); + $t = array_pop($parents); + } + if (!isset($highest_term) || $depth < $highest_term->depth) { + $term->depth = $depth; + $highest_term = $term; + } + } + $section_term = $highest_term; + } } + // "Highest-level ancestor." + elseif ($selection_criterion == 'ancestor') { + $first_term_result = $section_term_query->orderBy('ti.created') + ->range(0, 1) + ->execute(); + + if ($first_term_result->rowCount() > 0) { + $first_term_tid = $first_term_result->fetchField(); + $lineage = taxonomy_get_parents_all($first_term_tid); + $section_term = array_pop($lineage); + } + } + } + + if (property_exists($section_term, 'name')) { + $section_name = $section_term->name; } - // Allow any module to alter the section as it sees fit. - // @todo: Consider passing the section term as well, if applicable. - drupal_alter('parsely_section', $section_name, $node, $section_vocabulary); + // Allow any module to alter the section name. + drupal_alter('parsely_section', $section_name, $node, $section_term); return $section_name; } From 2be22596c5d20c01f87eff6273ed3935abc99de3 Mon Sep 17 00:00:00 2001 From: Nathanael Dewhurst Date: Thu, 19 Feb 2015 16:31:13 -0500 Subject: [PATCH 6/8] Support for thumbnailURL and future metadata management. New config section for metadata values, with node/global token support. Current iteration supports the "thumbnailUrl" field. Future iterations should support the custom "metadata" option, "@type," etc. (possibly all fields). Works gracefully with any recent version of Token module, or without it. --- parsely.admin.inc | 55 +++++++++++++++++++++++++++++++++++++++++++++++ parsely.module | 13 +++++++++-- 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/parsely.admin.inc b/parsely.admin.inc index 5e675eb..fa0db3d 100644 --- a/parsely.admin.inc +++ b/parsely.admin.inc @@ -60,6 +60,61 @@ function parsely_admin_settings($form, &$form_state) { } // TODO: Should display warning if taxonomy module isn't available + // Optional settings + $form['parsely_metadata'] = array( + '#type' => 'fieldset', + '#title' => t('Metadata Settings'), + '#description' => t('Customize the values reported to Parse.ly for each page. Read more about Parse.ly metadata !here.', array('!here' => l('here', 'http://www.parsely.com/docs/integration/metadata/ppage.html'))), + '#collapsible' => TRUE, + '#collapsed' => FALSE, + ); + // Thumbnail image. + $logo_url = ltrim(theme_get_setting('logo', variable_get('theme_default')), '/'); + $logo_url = url($logo_url, array('absolute' => TRUE)); + $form['parsely_metadata']['parsely_metadata_thumbnail_url'] = array( + '#type' => 'textfield', + '#title' => t('thumbnailUrl'), + '#description' => t('An image that will be used to illustrate a page/post in certain Parse.ly dashboard displays.'), + '#default_value' => variable_get('parsely_metadata_thumbnail_url', $logo_url), + '#maxlength' => 1024, + ); + if (module_exists('token')) { + $form['parsely_metadata']['#description'] .= ' ' . t('You can use tokens from the list below to specify dynamic patterns for each metadata item.'); + // Set up a token browser so admins can use relevant tokens to define + // metadata values. Update all textfields in this fieldset accordingly. + // Note: The ability to provide token_types param to theme_token_tree_link + // was introduced by the token module on 2014-06-19 in v. 7.x-1.5+4-dev. + // @see https://www.drupal.org/node/2289203. + $token_module_info = system_get_info('module', 'token'); + $token_version = $token_module_info['version']; + if (version_compare($token_version, '7.x-1.5+4-dev', 'ge')) { + $theme_function = 'token_tree_link'; + } + else { + $theme_function = 'token_tree'; + } + $form['parsely_metadata']['tokens'] = array( + '#theme' => $theme_function, + '#token_types' => array('node'), + '#global_types' => TRUE, + '#click_insert' => TRUE, + ); + + foreach (element_children($form['parsely_metadata']) as $field_key) { + $field = $form['parsely_metadata'][$field_key]; + if ($field['#type'] == 'textfield') { + // Note: because we're not requiring a minimum number of tokens for + // these fields, the only way they can fail validation is if they + // include a real token that belongs to a disallowed context. + $field['#element_validate'] = array( + 'token_element_validate', + ); + $field['#token_types'] = array('node'); + $form['parsely_metadata'][$field_key] = $field; + } + } + } + // Optional settings $form['parsely_optional_settings'] = array( '#type' => 'fieldset', diff --git a/parsely.module b/parsely.module index ab9cc5f..022d441 100644 --- a/parsely.module +++ b/parsely.module @@ -302,23 +302,32 @@ function _parsely_get_tags($node) { function _parsely_get_node_metadata() { $node = menu_get_object(); - // @todo: Integrate with metatag module and/or support tokens, etc. + // @todo: Integrate with metatag module and/or support more tokens, etc. $parsely_meta = array( '@context' => 'http://schema.org', '@type' => 'WebPage', 'articleId' => _parsely_get_content_id($node), 'headline' => $node->title, 'url' => _parsely_get_canonical_url($node), - 'thumbnailUrl' => 'FIX ME', 'dateCreated' => _parsely_get_pub_date_utc($node), 'articleSection' => _parsely_get_section($node), 'creator' => _parsely_get_authors($node), 'keywords' => _parsely_get_tags($node), ); + if ($thumbnail_url = variable_get('parsely_metadata_thumbnail_url', FALSE)) { + $thumbnail_url = token_replace($thumbnail_url, array('node' => $node)); + if (!empty($thumbnail_url)) { + $parsely_meta['thumbnailUrl'] = $thumbnail_url; + } + } + + // @todo: Allow admins to customize the @type value. if ($node->type === 'article') { $parsely_meta['@type'] = 'NewsArticle'; } + // @todo: Also support the "metadata" key so users can pass in any additional custom data. + return $parsely_meta; } From 8d6f0f922ededcd296840b25717f384e6b812db1 Mon Sep 17 00:00:00 2001 From: Nathanael Dewhurst Date: Thu, 19 Feb 2015 17:25:46 -0500 Subject: [PATCH 7/8] Making node metadata alterable --- parsely.module | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/parsely.module b/parsely.module index 022d441..f3fbe2b 100644 --- a/parsely.module +++ b/parsely.module @@ -329,5 +329,9 @@ function _parsely_get_node_metadata() { // @todo: Also support the "metadata" key so users can pass in any additional custom data. + // Allow modules to alter the default metadata for this node. + // Should this be more generic (rather than node-specific)? + drupal_alter('parsely_node_metadata', $parsely_meta, $node); + return $parsely_meta; } From ceff669be060fd17be74f6226971892ca7e2ece8 Mon Sep 17 00:00:00 2001 From: Nathanael Dewhurst Date: Thu, 26 Feb 2015 13:29:22 -0500 Subject: [PATCH 8/8] Making node author list alterable --- parsely.module | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/parsely.module b/parsely.module index f3fbe2b..1d27c36 100644 --- a/parsely.module +++ b/parsely.module @@ -170,7 +170,10 @@ function _parsely_get_canonical_url($node) { } function _parsely_get_authors($node) { - return array($node->name); + $authors = array($node->name); + drupal_alter('parsely_authors', $authors, $node); + + return $authors; } function _parsely_get_pub_date_utc($node) {