My goal is to allow exact, sticky, front-page only, positioning of posts based on user back-end input. All other WP behaviour should remain the default.
The front page displays 9 posts
User can select the position of the post on the front page (Off, 1-9)
Only one post can have an assigned position, the posts with the
latest post date is selected, others with the same positioned are
ignored and remain part of the regular orderby flow
In order to re-arrange my posts I run a second WP_Query to collect all posts that have a position assigned to them through a custom meta field. The meta field shows the option 1-9 on the admin post page but corresponds to the index numbers of an array, just like the WP_Query object posts array.
function get_positioned_posts() {
$positioned_posts = get_posts( array (
'meta_query' => array(
array(
'key' => 'post_position',
'value' => array('0','1','2','3','4','5','6','7','8'),
'compare' => 'IN',
),
),
));
return $positioned_posts;
}
Subsequently I get the MAIN posts array, loop over it and insert a positioned post when the index number matches the positioned_post value.
function mns_post_position( $posts) {
if ( is_home() && is_main_query() && ! is_admin() && ! is_search() && ! is_archive()) {
$positioned_posts = get_positioned_posts();
$positioned_post_ids = wp_list_pluck( $positioned_posts, 'ID' );
$num_posts = count( $positioned_posts );
// Find the positioned posts
for ($i = 0; $i < $num_posts; $i++) {
if ( in_array( $positioned_posts[$i]->ID, $positioned_post_ids ) ) {
$positioned_post = $positioned_posts[$i];
// Remove post from current position
array_splice( $posts, 4, 1 );
// Get post position and put it in the posts array
$position = intval(get_post_meta( $positioned_post->ID, 'post_position', true ));
array_splice($posts, $position, 0, array($positioned_post));
}
}
}
return $posts;
}
add_action('the_posts', 'mns_post_position', 1 );
This works great, but it only works for the homepage and not the paged pages. The "the_posts" action only gets the current X (9) posts and not the entire WP Query posts array. This causes unwanted behaviour:
There can be more than 9 posts on the frontpage if a positioned posts
comes from page > 2.
You can see a positioned posts twice, once on the frontpage and later
one a paged page.
Possible solutions
I need to find a way to build the query correctly on the first try.
I don't see how I can achieve this with pre_get_posts filter.
Using "menu_order" like some plugins is not an option. Ordering by date is required.
Find a hook that returns the ENTIRE WP_Query Posts array, which I
just rearrange with array_splice.
Related
I have following structure
Services
-Page_1
-Page_2
--Page_3
--Page_4
I use Wp-query to get pages. This is my args:
<?php $args = array(
'post_type' => 'page',
'post_parent' => '45'
); ?>
45 is ID of Services page.
But I only get first level Page_1 and Page_2. How do I get all pages? I'm using Advanced Custom Fields plugin so using get_pages() is not a good option, is it?
As mentioned in your comments, the easiest solutions is to actually use get_pages(). You can still grab meta data of posts if you are able to get the the ID of the page (which you can with get_pages()) so you should still be able to ACF's custom fields.
Here's an example of get_pages():
$args = array(
'child_of' => 45,
'post_type' => 'page',
);
$pages = get_pages($args);
The main difference between get_pages() and WP_Query that we want to focus on here is the 'child-of'=>45 paramater vs the 'post_parent'=>45.
The child-of argument will grab ALL children throughout the hierarchy, i.e. children and children's children etc.
In contrast, the post-parent argument of WP_Query will only grab direct children of the page.
Using in conjunction with ACF
If you need to grab custom fields from ACF, you will still be able to use get_post_meta().
If your custom field includes an array of values, you will need to unserialize it first and then loop through the values like this:
$meta = get_post_meta( $post->ID, 'acf_meta_key', true ); //grab the post meta using WordPress core function
$array = unserialize( $meta ); //unserialize the field into an array
foreach ( $array as $value ) { //loop through the array
echo $value .'<br>';
}
I have a pretty complicated query that I have not been able to get to work the way I need it to.
I have a Wordpress install using the plugins WP Courseware and ACF. I need to display a page of courses associated with the current user. I want the links to lead the user to the course "home" pages that the user should hit prior to starting the course. I have created course "home" pages, but the problem is WP Courseware has no way to associate a page with a course. So I had to use an ACF options repeater that associates the course ID with whatever course pages are necessary. The only way I know that one of those associated pages is the course "home" page is by the template I use for course home pages.
So the loops within loops need to first determine what courses the current user has access to, get those course IDs, loop the ACF options repeater to find the pages associated with those course IDs, then of those pages loop to find out which one (there is only one per course) uses the course home page template. This last loop I discovered needs to be a WP_Query loop as that's the only way to query for a Wordpress template.
I am lost in loops and I'm having the hardest time. I thought it might be simpler and most direct to use the WP_Query to query an array meta_queries of both the Wordpress template and the ACF repeater (to determine is the ACF repeater course ID matches the course ID the user has access to) but my attempts at querying ACF repeater sub fields is not working.
Here's my code:
$user = wp_get_current_user();
$user_id = $user->ID;
$user_course_list = WPCW_users_getUserCourseList($user_id);
$course_association_arr = get_field('course_association', 'option');
// Loop through user's courses
foreach ( $user_course_list as $user_course ) :
$course_id = $user_course->course_id;
$course_title = $user_course->course_title;
// Loop through the ACF course ID/page associations
foreach ( $course_association_arr as $course_association ) :
$assoc_course_id = $course_association['wp_courseware_id'];
if ( $course_id == $assoc_course_id ) :
// Loop through the ACF associated pages
foreach ( $course_association['associated_pages'] as $associated_page ) :
$page_id = $associated_page->ID;
$page_url = $associated_page->guid;
echo '<li>'. $course_title . '</li>';
endforeach;
endif;
endforeach;
endforeach;
This displays all pages associated with a user's courses, not just the ones using the course home template. I somehow have to incorporate a WP_Query with these args in there and nothing I have done has worked:
$args = array(
'post_type' => 'page',
'meta_query' => array(
array(
'key' => '_wp_page_template',
'value' => 'page-course-home.php',
),
)
);
If I could somehow turn the WP query into an if statement (if template = page-course-home.php) I could have that inside the associated pages query to only show course home pages. Or there maybe another more brilliant way to do what I need to do. I appreciate all feedback.
Ok I got something to work! I think spending so much time framing the question here helped me see one way I could do it:
$user = wp_get_current_user();
$user_id = $user->ID;
$user_course_list = WPCW_users_getUserCourseList($user_id);
$course_association_arr = get_field('course_association', 'option');
// Loop through user's courses
foreach ( $user_course_list as $user_course ) :
$course_id = $user_course->course_id;
$course_title = $user_course->course_title;
// Loop through the ACF course ID/page associations
foreach ( $course_association_arr as $course_association ) :
$assoc_course_id = $course_association['wp_courseware_id'];
if ( $course_id == $assoc_course_id ) :
// Loop through the ACF associated pages
foreach ( $course_association['associated_pages'] as $associated_page ) :
$page_id = $associated_page->ID;
$page_url = $associated_page->guid;
$args = array(
'post_type' => 'page',
'page_id' => $page_id,
'meta_query' => array(
array(
'key' => '_wp_page_template',
'value' => 'page-course-home.php',
),
)
);
$course_assoc_pages = new WP_Query( $args );
if( $course_assoc_pages->have_posts() ) :
while ( $course_assoc_pages->have_posts() ) : $course_assoc_pages->the_post();
echo '<li>'. $course_title . '</li>';
endwhile;
endif;
wp_reset_query();
endforeach;
endif;
endforeach;
endforeach;
This seems a bit cumbersome, but it works. I'm not sure if it would be better but it seems more elegant to incorporate the ACF subfield query into the meta query, so could eliminate two of the loops. If anyone has any thoughts on this I would love to hear them.
Trying to recursively select database fields based on parent IDs of current selection in a WordPress environment.
On the WordPress side:
I'm doing a pre_get_posts filter to retrieve only the posts associated with a user meta. This was simple, until I realized that these posts could have parents and that would break the interface. Answers involving fixing the WP_List_Table sorting algorithm to allow children to be re-sorted hierarchically may be accepted. Without including parents, the table looks like this:
The meta query looks like this:
// pre_get_posts
$meta_query = $query->get('meta_query');
$meta_query[] = array(
'key' => '_my_meta_key',
'value' => get_user_meta( $user_id, '_my_meta_key' ),
);
$query->set('meta_query', $meta_query );
On the MySQL side, adding the parents to this query would work just as well. As it stands, the query above works like the first select clause of the SQL fiddle here:
SQL Fiddle
A) In my limited SQL knowledge, I haven't figured out how to use the first query's id column as a parameter. Here, I've copy and pasted the entire thing to use as an IN parameter, which runs the query twice. Is there a way to alias the first query to use the id field in the second query?
B) How can I also include the parents of the parents (and so on) returned in the second SELECT statement?
I ended up manually adding the parents back into the result after wp_query is parsed. So far, I haven't run into any issues. It's not exactly what I wanted, as it produces extra (not too many in this case) and thought there might be a more efficient way, but it is what it is. I'll leave this open a while in case anyone comes up with anything better.
Note that since the user cannot edit these posts, they are not editable (awesome!)
Example code:
// users must see parent pages on the Edit screen
// even when they can't edit them
// to preserve hierarchy
add_action( 'the_posts', 'add_post_parents_not_in_meta_query'), 10, 2 );
function add_post_parents_not_in_meta_query( $posts, $query ) {
// only on pages with screen
if ( ! function_exists( 'get_current_screen' ) )
return $posts;
$screen = get_current_screen();
// only on edit screen
if ( $screen->base !== 'edit')
return $posts;
// only during main query
if ( ! $query->is_main_query() )
return;
// get an array of found IDs
$post_ids = wp_list_pluck( $posts, 'ID' );
foreach( $posts as $post ) {
// check to see if exists, and if we already got this post
while ( ! empty( $post->post_parent ) && ! in_array( $post->post_parent, $post_ids ) ) {
$post = get_post( $post->post_parent );
if ( ! empty( $post ) ) {
$posts[] = $post;
$post_ids[] = $post->ID;
}
}
}
return $posts;
}
Result:
I'm trying something out with Wordpress. I want to use page as a shell for custom content, so I've setup an action that adds a specific meta_key for each regular page, so that I can single out the regular pages from my 'special-pages'.
function addMetaToPage($post_id) {
if ( wp_is_post_revision( $post_id ) )
return;
if(get_post_type($post_id) == 'page') {
add_post_meta($post_id, '_regular_page', 1, true);
}
}
if(is_admin()) {
add_action('save_post', 'addMetaToPage');
}
Then, in the admin backend where pages are listed I run this hook to prevent all my 'special-pages' to show up.
if(is_admin()) {
add_action('pre_get_posts', function($query) {
$query->set('meta_key', '_regular_page');
$query->set('meta_value', 1);
});
}
And it works. Only pages with meta _regular_page === 1 shows up. However, the counter just above the table that usually sais something like All (15) shows the total number of pages, even though only a couple of them are in the list. Check this screenshot. At this page I got 4 regular pages and 6 pages that dosn't have the _regular_page meta key.
Is there a way to actually fix this? I really thought that the counter was dependent on the WP_Query, but apparently not.
you can edit the count of ALL, Published and trash for the page section in the admin using the filter views_edit-page
you can use something like this
add_filter( "views_edit-page", "filter_regular_page_count", 10, 1);
function filter_regular_page_count( $views ) {
$regular_post_count = 5 ;// you can sort out how to get your regular post count
$views["all"] = 'All <span class="count">('.$regular_post_count.')</span>';
//same for published and trash
return $views;
}
here is example of var dump of views
array (size=3)
'all' => string 'All <span class="count">(7)</span>' (length=88)
'publish' => string '(7)</span>' (length=102)
'trash' => string '(2)</span>' (length=96)
if you want to do the same thing for the posts, WordPress use views_edit-post filter for the posts
actually wordpress use something like this as the filter of the views section ---> views_{$this->screen->id}
I am using Wordpress auto suggests using this snippet of code
and currently it is searching all tags, I want it to search only post titles. Any help is appreciated.
This is sql query calling all the tags which needs to be modified for all posts.
<?php global $wpdb;
$search_tags = $wpdb->get_results("SELECT name from wp_terms WHERE name LIKE '$search%'");
foreach ($search_tags as $mytag)
{
echo $mytag->name. " ";
}
?>
These days i had to do some request in a wordpress theme.
In your case ( getting title can be done easier than getting tags, as in your example link ) the stuff can be done easier (i guess).
Firstly you have to make a php page to get posts. As you maybe know you won't be able to use wp stuff in standalone php files, so your file ( let call it get_posts.php ) will look like
<?php
// Include the file above for being able to use php stuff
// In my case this file was in a folder inside my theme ( my_theme/custom_stuff/get_posts.php ).
// According to this file position you can modify below path to achieve wp-blog-header.php from wp root folder
include( '../../../../wp-blog-header.php' );
// Get all published posts.
$list_posts = get_posts( array( 'numberposts' => -1 ) );
// Get "q" parameter from plugin
$typing = strtolower( $_GET["q"] );
//Save all titles
$list_titles = array();
foreach( $list_posts as $post ) { $list_titles[] = $post->post_title; }
// To see more about this part check search.php from example
foreach( $list_titles as $title ) {
if( strpos( strtolower( $title ), $typing ) ){
echo $title;
}
}
?>
I added some comments trying to help you better.
Now stuff get easy, you only have to call your page through jQuery plugin like
$('#yourInput').autocomplete( "path_to/get_posts.php" );
You can directly use wordpress in-build feature to get all post titles
// The Query
query_posts( 'posts_per_page=-1' );
// The Loop
while ( have_posts() ) : the_post();
echo '<li>';
the_title();
echo '</li>';
endwhile;
None of the answers here answer your real question:
How to query JUST post titles
The "raw SQL" way:
Few important things:
escape search for SQL! (also do that for the tags search!) use $GLOBALS['wpdb']->esc_like()
if you only need 1 column, you can use $GLOBALS['wpdb']->get_col()$GLOBALS['wpdb']->get_results() is if you want to fetch more columns in one row
use $GLOBALS['wpdb']->tableBaseName to make your code portable - takes care of the prefixe.g. $GLOBALS['wpdb']->posts
When querying posts you must also think about which post_type and post_status you want to query=> usually the post_status you want ispublish, but post_type may vary based on what you want
WordPress table "posts" contains ALL post types - post, page, custom, but also navigation, contact forms etc. could be there! => I strongly advice to use explicit post_type condition(s) in WHERE ! :)
...$GLOBALS is same as globalizing variabl -today performance difference is little
<?php
// to get ALL published post titles of ALL post types (posts, pages, custom post types, ...
$search_post_titles = $GLOBALS['wpdb']->get_col(
"SELECT post_title from {$GLOBALS['wpdb']->posts}
WHERE (
(post_status = 'publish')
AND
(post_title LIKE '{$GLOBALS['wpdb']->esc_like($search)}%')
)
ORDER BY post_title ASC
"); // I also added ordering by title (ascending)
// to only get post titles of Posts(Blog)
// you would add this to conditions inside the WHERE()
// AND (post_type = 'post')
// for Posts&Pages
// AND ((post_type = 'post') OR (post_type = 'page'))
// test output:
foreach ($search_post_titles as $my_title) {
echo $my_title . " ";
}
?>
The WP_Query way
This is more wordpress but has a little overhead, because although there is a fields param for new WP_Query()/get_posts(), it only has options:
'all' - all fields (also default), 'ids' - just ids, 'id=>parent' - ... if you pass anything else, you still get all, so you still need to add "all" BUT - WP however has filters for altering fields in SELECT.
I tried to make it the same as the raw SQL version, but it depends on how does WP does it's "search" - which I think is %search% for 1 word + some more logic if there are more words. You could leverage the $clauses filter used for fields to also add your custom where INSTEAD of adding the 's' into $args (remember to append to not-lose existing WHEREs $clauses['where' .= "AND ...;). Also post_type => 'any' does not produce always the same results as the raw query in cases like Navigation, Contact forms etc...
Also WP_Query sanitizes the input variables so actually don't escape $args!
<?php
$args = [
'fields' => 'all', // must give all here and filter SELECT(fields) clause later!
'posts_per_page' => -1, // -1 == get all
'post_status' => 'publish',
's' => $search,
// I also added ordering by title (ascending):
'orderby' => 'title',
'order' => 'ASC',
'post_type' => 'any', // all "normal" post types
// 'post_type' => 'post', // only "blog" Posts
// 'post_type' => ['post', 'page'], // only blog Posts & Pages
];
$onlyTitlesFilter = function($clauses, $query) {
// "fields" overrides the column list after "SELECT" in query
$clauses['fields'] = "{$GLOBALS['wpdb']->posts}.post_title";
return $clauses; // !FILTER! MUST RETURN!
};
$onlyTitlesFilterPriority = 999;
// add filter only for this query
add_filter('posts_clauses', $onlyTitlesFilter, $onlyTitlesFilterPriority, 2);
// Pro-tip: don't use variable names like $post, $posts - they conflict with WP globals!
$my_posts = (new WP_Query($args))->get_posts();
// !IMPORTANT!
// !remove the filter right after so we don't affect other queries!
remove_filter('posts_clauses', $onlyTitlesFilter, $onlyTitlesFilterPriority);
// test output:
// note that I used "my"_post NOT just $post (that's a global!)
foreach($my_posts as $my_post) {
echo $my_post->post_title . " ";
}
?>
Don't be confused - you will still get the array of WP_Posts - WP will throw some default properties&values into it, but in reality it will only query and fill-in with real values the fields you specify in the filter.
PS: I've tested these - they are working codes (at least on WP 5.4 and PHP7 :) )