$columns['cb'] ) : array(); return array_merge( $checkbox, array( 'title' => __( 'Topic', 'saidweekly' ), 'swcc_keyword' => __( 'Target keyword', 'saidweekly' ), 'swcc_category' => __( 'Category', 'saidweekly' ), // The article's intended author, not the topic's post_author. Whoever typed the // topic in is on the topic editor; this column answers the question the calendar // is actually asked, which is who the article will be credited to. 'swcc_author' => __( 'Author', 'saidweekly' ), self::COL_SCHEDULED => __( 'Scheduled', 'saidweekly' ), 'swcc_status' => __( 'Status', 'saidweekly' ), 'swcc_job' => __( 'Generation job', 'saidweekly' ), 'swcc_generated' => __( 'Generated post', 'saidweekly' ), ) ); } /** * Mark which columns are sortable. * * @param array $columns Existing sortable columns. * @return array */ public static function sortable_columns( $columns ) { $columns[ self::COL_SCHEDULED ] = self::COL_SCHEDULED; return $columns; } /** * Output a single cell. * * @param string $column Column key. * @param int $post_id Topic post ID. * @return void */ public static function render_column( $column, $post_id ) { switch ( $column ) { case 'swcc_keyword': $keyword = (string) get_post_meta( $post_id, SWCC_Topic::META_KEYWORD, true ); echo '' !== $keyword ? esc_html( $keyword ) : '—'; break; case 'swcc_category': $term_id = (int) get_post_meta( $post_id, SWCC_Topic::META_CATEGORY, true ); $term = $term_id > 0 ? get_term( $term_id, 'category' ) : null; echo $term instanceof WP_Term ? esc_html( $term->name ) : '—'; break; case self::COL_SCHEDULED: $scheduled = (string) get_post_meta( $post_id, SWCC_Topic::META_SCHEDULED, true ); if ( '' === $scheduled ) { echo '—'; break; } // Built in the site's own time zone, not parsed in PHP's. strtotime() resolves a // bare date against date_default_timezone_get(), which WordPress leaves at UTC, // while wp_date() then formats in the site's zone - so on any site west of UTC // every scheduled date rendered a day early. The stored value is date-only and // means that calendar day, so it has to be anchored to that day locally. $scheduled_local = DateTimeImmutable::createFromFormat( '!Y-m-d', $scheduled, wp_timezone() ); if ( false === $scheduled_local ) { echo '—'; break; } echo esc_html( wp_date( (string) get_option( 'date_format' ), $scheduled_local->getTimestamp() ) ); break; case 'swcc_author': $author_name = SWCC_Settings::author_name( SWCC_Topic::get_author_id( $post_id ) ); echo '' !== $author_name ? esc_html( $author_name ) : '—'; // Quick Edit clones one template row for whichever topic was clicked, so it needs // this row's own values from somewhere. Core writes its equivalent per row too. SWCC_Topic_Quick_Edit::render_inline_data( $post_id ); break; case 'swcc_status': $status = SWCC_Topic::sanitize_status( get_post_meta( $post_id, SWCC_Topic::META_STATUS, true ) ); printf( '%2$s', esc_attr( $status ), esc_html( SWCC_Topic::get_status_label( $status ) ) ); // A planned topic with no target keyword is skipped by dispatch. Saying so here // is the whole point of skipping it rather than sending it: silently omitting an // entry from the calendar's own run would be as confusing as the site-wide // failure it replaces. if ( 'planned' === $status && ! SWCC_Topic::is_ready_to_generate( $post_id ) ) { printf( '
%s', esc_html__( 'Needs a target keyword', 'saidweekly' ) ); } // P3 S1: a lock and a hold are orthogonal to the workflow status, so they sit // beside it rather than replacing it. The hold's full sentence is the tooltip; // the editor shows it in full with the actions that clear it. if ( SWCC_Topic::is_locked( $post_id ) ) { printf( ' ', esc_attr__( 'Locked: never changed by calendar generation', 'saidweekly' ) ); } $hold_sentence = SWCC_Topic::get_hold_sentence( $post_id ); if ( '' !== $hold_sentence ) { printf( '
%2$s', esc_attr( $hold_sentence ), esc_html__( 'On hold', 'saidweekly' ) ); } // Item-level status text (P3 S5): the last failed attempt's sentence, and a planned // topic that cannot be written because the site has no credits to spend. $last_error = (string) get_post_meta( $post_id, SWCC_Rest_Job_Failed::META_LAST_ERROR, true ); if ( 'planned' === $status && '' !== $last_error ) { printf( '
%2$s', esc_attr( $last_error ), esc_html__( 'Last attempt failed', 'saidweekly' ) ); } $balance = SWCC_Billing::last_known_balance(); // Not on the free plan: its credit is minted when it is used, so it never holds one. if ( 'planned' === $status && is_array( $balance ) && ! empty( $balance['plan'] ) && 'free' !== $balance['plan'] && 0 === (int) $balance['spendable'] ) { printf( '
%s', esc_html__( 'Needs credits', 'saidweekly' ) ); } break; case 'swcc_generated': $generated = (int) get_post_meta( $post_id, SWCC_Topic::META_GENERATED, true ); if ( $generated > 0 && get_post( $generated ) instanceof WP_Post ) { printf( '%2$s', esc_url( (string) get_edit_post_link( $generated ) ), esc_html( get_the_title( $generated ) ) ); } else { echo '—'; } break; case 'swcc_job': $job_id = (string) get_post_meta( $post_id, SWCC_Topic::META_JOB_ID, true ); echo '' !== $job_id ? '' . esc_html( $job_id ) . '' : '—'; break; } } /** * Render the status filter dropdown above the list table. * * @param string $post_type Post type of the current list table. * @return void */ public static function render_status_filter( $post_type ) { if ( SWCC_Topic::POST_TYPE !== $post_type ) { return; } $current = self::requested_status(); ?> is_main_query() ) { return; } if ( SWCC_Topic::POST_TYPE !== $query->get( 'post_type' ) ) { return; } $meta_query = $query->get( 'meta_query' ); $meta_query = is_array( $meta_query ) ? $meta_query : array(); $status = self::requested_status(); if ( '' !== $status ) { $meta_query['swcc_status_filter'] = array( 'key' => SWCC_Topic::META_STATUS, 'value' => $status, 'compare' => '=', ); } $orderby = $query->get( 'orderby' ); // No explicit sort requested: default to soonest-due first, so the next thing up // is at the top of the screen, rather than falling back to WP's own date-published // default. if ( '' === $orderby ) { $orderby = self::COL_SCHEDULED; $query->set( 'order', 'ASC' ); } if ( self::COL_SCHEDULED === $orderby ) { // A named meta_query clause is used rather than the `meta_key` query var so // that filtering and sorting cannot produce two joins on the same key. $meta_query['swcc_scheduled_sort'] = array( 'key' => SWCC_Topic::META_SCHEDULED, 'compare' => 'EXISTS', 'type' => 'DATE', ); $order = strtoupper( (string) $query->get( 'order' ) ); $order = 'ASC' === $order ? 'ASC' : 'DESC'; $query->set( 'orderby', array( 'swcc_scheduled_sort' => $order ) ); } if ( ! empty( $meta_query ) ) { $query->set( 'meta_query', $meta_query ); } } /** * Read the requested status filter from the request. * * @return string A valid status key, or an empty string for "all". */ private static function requested_status() { // A list table filter is a read-only GET with no state change, so no nonce applies. // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( ! isset( $_GET[ self::FILTER_STATUS ] ) ) { return ''; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended $raw = sanitize_key( wp_unslash( $_GET[ self::FILTER_STATUS ] ) ); return array_key_exists( $raw, SWCC_Topic::get_statuses() ) ? $raw : ''; } }