diff --git a/src/wp-admin/css/dashboard.css b/src/wp-admin/css/dashboard.css index 892d309de4da6..1f1207606439e 100644 --- a/src/wp-admin/css/dashboard.css +++ b/src/wp-admin/css/dashboard.css @@ -1017,6 +1017,35 @@ body #dashboard-widgets .postbox form .submit { top: 0; } +/* On This Day dashboard widget */ + +#wp_dashboard_on_this_day h3 { + font-weight: 600; +} + +#wp_dashboard_on_this_day li { + margin: 0; + padding: 0; +} + +#wp_dashboard_on_this_day ul ul { + margin: 0 0 0 18px; + padding: 0; + list-style: disc; +} + +#wp_dashboard_on_this_day ul ul li + li { + margin-top: 6px; +} + +#wp_dashboard_on_this_day .wp-on-this-day-widget > ul > li + li { + margin-top: 16px; +} + +#wp_dashboard_on_this_day .wp-on-this-day-post-author { + color: #646970; +} + /* Browse happy box */ #dashboard-widgets #dashboard_browser_nag.postbox .inside { diff --git a/src/wp-admin/includes/dashboard-on-this-day.php b/src/wp-admin/includes/dashboard-on-this-day.php new file mode 100644 index 0000000000000..1939f8ca4b0e3 --- /dev/null +++ b/src/wp-admin/includes/dashboard-on-this-day.php @@ -0,0 +1,238 @@ +' . esc_html__( 'No posts were published on this day in previous years.' ) . '

'; + return; + } + + $posts_by_year = array(); + $post_count = count( $posts ); + + foreach ( $posts as $post ) { + $year = get_the_date( 'Y', $post ); + + if ( ! isset( $posts_by_year[ $year ] ) ) { + $posts_by_year[ $year ] = array(); + } + + $posts_by_year[ $year ][] = $post; + } + + /* translators: Date format for the On This Day widget date, without year. See https://www.php.net/manual/datetime.format.php */ + $date = '' . esc_html( wp_date( _x( 'F jS', 'on this day date format' ) ) ) . ''; + ?> +
+

+ +

+ +
+ format( 'Y' ); + $date_query = array( + 'relation' => 'AND', + array( + 'before' => array( 'year' => $year ), + ), + _wp_dashboard_on_this_day_date_query_clause( $today ), + ); + + $args = array( + 'post_type' => 'post', + 'post_status' => array( 'publish' ), + 'posts_per_page' => 10, + 'ignore_sticky_posts' => true, + 'orderby' => 'date', + 'order' => 'DESC', + 'no_found_rows' => true, + 'update_post_term_cache' => false, + 'update_post_meta_cache' => false, + 'date_query' => $date_query, + ); + + /** + * Filters the arguments used to query posts for the On This Day dashboard widget. + * + * @since 7.1.0 + * + * @param array $args WP_Query arguments. + */ + $args = apply_filters( 'wp_dashboard_on_this_day_query_args', $args ); + + $query = new WP_Query( $args ); + + return $query->posts; +} + +/** + * Builds the date query clause for today's anniversary date. + * + * On February 28 in a non-leap year, February 29 posts are included so + * leap-day anniversaries still appear. + * + * @since 7.1.0 + * @access private + * + * @param DateTimeInterface $date Date to build the clause for. + * @return array Date query clause. + */ +function _wp_dashboard_on_this_day_date_query_clause( $date ) { + $month = (int) $date->format( 'm' ); + $day = (int) $date->format( 'd' ); + $clause = array( + 'month' => $month, + 'day' => $day, + ); + + // Display leap day posts on Feb 28 in non leap years. + if ( + 28 === $day + && 2 === $month + && false === (bool) $date->format( 'L' ) + ) { + $clause = array( + 'relation' => 'OR', + $clause, + array( + 'month' => 2, + 'day' => 29, + ), + ); + } + + return $clause; +} diff --git a/src/wp-admin/includes/dashboard.php b/src/wp-admin/includes/dashboard.php index d88e6b87649f2..a0b0ac6c77239 100644 --- a/src/wp-admin/includes/dashboard.php +++ b/src/wp-admin/includes/dashboard.php @@ -88,6 +88,13 @@ function wp_dashboard_setup() { wp_add_dashboard_widget( 'dashboard_quick_press', $quick_draft_title, 'wp_dashboard_quick_press' ); } + // On This Day. + if ( ! function_exists( 'wp_dashboard_on_this_day_setup' ) ) { + require_once ABSPATH . 'wp-admin/includes/dashboard-on-this-day.php'; + } + + wp_dashboard_on_this_day_setup(); + // WordPress Events and News. wp_add_dashboard_widget( 'dashboard_primary', __( 'WordPress Events and News' ), 'wp_dashboard_events_news' ); diff --git a/tests/phpunit/tests/admin/wpOnThisDay.php b/tests/phpunit/tests/admin/wpOnThisDay.php new file mode 100644 index 0000000000000..61aef6401683a --- /dev/null +++ b/tests/phpunit/tests/admin/wpOnThisDay.php @@ -0,0 +1,333 @@ +modify( '-' . $years_ago . ' years' )->format( 'Y-m-d' ) . ' ' . $time; + + return self::factory()->post->create( + array( + 'post_author' => $author_id, + 'post_date' => $post_date, + 'post_date_gmt' => get_gmt_from_date( $post_date ), + 'post_status' => 'publish', + 'post_title' => $title, + ) + ); + } + + /** + * Creates a published post near, but not on, today's prior-year calendar day. + * + * @param int $author_id Author ID. + * @param string $title Post title. + * @param int $day_offset Number of days from today's prior-year calendar day. + * @return int Post ID. + */ + private function create_nearby_post( $author_id, $title = 'Almost a memory', $day_offset = 1 ) { + $post_date = current_datetime() + ->modify( '-1 year' ) + ->modify( ( $day_offset >= 0 ? '+' : '' ) . $day_offset . ' days' ) + ->format( 'Y-m-d' ) . ' 12:00:00'; + + return self::factory()->post->create( + array( + 'post_author' => $author_id, + 'post_date' => $post_date, + 'post_date_gmt' => get_gmt_from_date( $post_date ), + 'post_status' => 'publish', + 'post_title' => $title, + ) + ); + } + + /** + * Invokes _wp_dashboard_on_this_day_date_query_clause(). + * + * @param string $date Date string. + * @return array Date query clause. + */ + private static function get_date_query_clause( $date ) { + return _wp_dashboard_on_this_day_date_query_clause( new DateTimeImmutable( $date, wp_timezone() ) ); + } + + /** + * @covers ::wp_dashboard_on_this_day_setup + */ + public function test_setup_always_registers_widget_and_postbox_class_filter() { + $this->set_up_dashboard_screen(); + + $user_id = self::factory()->user->create( array( 'role' => 'author' ) ); + wp_set_current_user( $user_id ); + + wp_dashboard_on_this_day_setup(); + + $dashboard_widgets = $GLOBALS['wp_meta_boxes']['dashboard']['normal']['core'] ?? array(); + + $this->assertArrayHasKey( 'wp_dashboard_on_this_day', $dashboard_widgets ); + $this->assertSame( 'On This Day', $dashboard_widgets['wp_dashboard_on_this_day']['title'] ); + $this->assertNotFalse( + has_filter( + 'postbox_classes_dashboard_wp_dashboard_on_this_day', + 'wp_dashboard_on_this_day_postbox_classes' + ) + ); + } + + /** + * @covers ::wp_dashboard_on_this_day_postbox_classes + */ + public function test_postbox_classes_hides_widget_without_matching_posts() { + $user_id = self::factory()->user->create( array( 'role' => 'author' ) ); + wp_set_current_user( $user_id ); + + $this->assertContains( 'hidden', wp_dashboard_on_this_day_postbox_classes( array( '' ) ) ); + } + + /** + * @covers ::wp_dashboard_on_this_day_postbox_classes + */ + public function test_postbox_classes_does_not_hide_widget_with_matching_posts() { + $user_id = self::factory()->user->create( array( 'role' => 'author' ) ); + wp_set_current_user( $user_id ); + $this->create_matching_post( $user_id ); + + $this->assertNotContains( 'hidden', wp_dashboard_on_this_day_postbox_classes( array( '' ) ) ); + } + + /** + * @covers ::wp_dashboard_on_this_day_setup + */ + public function test_setup_adds_dashboard_widget_with_matching_post_from_another_author() { + $this->set_up_dashboard_screen(); + + $user_id = self::factory()->user->create( array( 'role' => 'author' ) ); + $other_user_id = self::factory()->user->create( array( 'role' => 'author' ) ); + wp_set_current_user( $user_id ); + $this->create_matching_post( $other_user_id ); + + wp_dashboard_on_this_day_setup(); + + $dashboard_widgets = $GLOBALS['wp_meta_boxes']['dashboard']['normal']['core'] ?? array(); + + $this->assertArrayHasKey( 'wp_dashboard_on_this_day', $dashboard_widgets ); + } + + /** + * @covers ::_wp_dashboard_on_this_day_date_query_clause + */ + public function test_get_date_query_clause_includes_february_29_on_february_28_in_non_leap_year() { + $clause = self::get_date_query_clause( '2023-02-28 12:00:00' ); + + $this->assertSame( + array( + 'relation' => 'OR', + array( + 'month' => 2, + 'day' => 28, + ), + array( + 'month' => 2, + 'day' => 29, + ), + ), + $clause + ); + } + + /** + * @covers ::_wp_dashboard_on_this_day_date_query_clause + */ + public function test_get_date_query_clause_does_not_include_february_29_on_february_28_in_leap_year() { + $clause = self::get_date_query_clause( '2024-02-28 12:00:00' ); + + $this->assertSame( + array( + 'month' => 2, + 'day' => 28, + ), + $clause + ); + } + + /** + * @covers ::_wp_dashboard_on_this_day_date_query_clause + */ + public function test_get_date_query_clause_matches_february_29_on_leap_day() { + $clause = self::get_date_query_clause( '2024-02-29 12:00:00' ); + + $this->assertSame( + array( + 'month' => 2, + 'day' => 29, + ), + $clause + ); + } + + /** + * @covers ::wp_dashboard_on_this_day + */ + public function test_widget_outputs_placeholder_without_matching_posts() { + $user_id = self::factory()->user->create( array( 'role' => 'author' ) ); + wp_set_current_user( $user_id ); + + ob_start(); + wp_dashboard_on_this_day(); + $output = ob_get_clean(); + + $this->assertStringContainsString( 'No posts were published on this day in previous years.', $output ); + $this->assertStringNotContainsString( '