$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 : '';
}
}