|null */
private ?array $old = null;
public function __construct( Repository $repo ) {
$this->repo = $repo;
}
public function hooks(): void {
add_action( 'admin_menu', [ $this, 'menu' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue' ] );
add_action( 'admin_post_poller_delete_poll', [ $this, 'handle_delete' ] );
add_action( 'admin_post_poller_set_status', [ $this, 'handle_status' ] );
add_action( 'admin_post_poller_duplicate_poll', [ $this, 'handle_duplicate' ] );
add_action( 'admin_post_poller_reset_poll', [ $this, 'handle_reset' ] );
add_action( 'admin_post_poller_hide_word', [ $this, 'handle_hide_word' ] );
}
public function menu(): void {
$this->list_hook = (string) add_menu_page(
__( 'Polenta', 'polenta' ),
__( 'Polenta', 'polenta' ),
'manage_options',
'poller',
[ $this, 'render_list' ],
'dashicons-forms',
58
);
add_submenu_page(
'poller',
__( 'All polls', 'polenta' ),
__( 'All polls', 'polenta' ),
'manage_options',
'poller',
[ $this, 'render_list' ]
);
$this->edit_hook = (string) add_submenu_page(
'poller',
__( 'Add poll', 'polenta' ),
__( 'Add poll', 'polenta' ),
'manage_options',
'poller-edit',
[ $this, 'render_edit' ]
);
add_action( 'load-' . $this->edit_hook, [ $this, 'handle_save' ] );
}
public function enqueue( string $hook ): void {
if ( $hook !== $this->list_hook && $hook !== $this->edit_hook ) {
return;
}
wp_enqueue_style( 'poller-fonts', POLLER_URL . 'assets/css/fonts.css', [], POLLER_VERSION );
wp_enqueue_style( 'poller-admin', POLLER_URL . 'assets/css/admin.css', [ 'poller-fonts' ], POLLER_VERSION );
wp_enqueue_script( 'poller-admin', POLLER_URL . 'assets/js/admin.js', [], POLLER_VERSION, true );
wp_localize_script(
'poller-admin',
'pollerAdmin',
[
'choose' => __( 'Choose a picture', 'polenta' ),
'use' => __( 'Use this picture', 'polenta' ),
'copied' => __( 'Copied', 'polenta' ),
]
);
if ( $hook === $this->edit_hook ) {
wp_enqueue_media();
}
}
public function handle_save(): void {
$method = isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : '';
if ( 'POST' !== $method ) {
return;
}
check_admin_referer( 'poller_save_poll' );
$this->guard();
$input = $this->posted_poll();
$existing = null;
if ( $input['id'] ) {
$existing = $this->repo->get_poll( $input['id'] );
if ( ! $existing ) {
$this->error = __( 'That poll does not exist.', 'polenta' );
$this->old = $input;
return;
}
}
$locked = $existing && $this->repo->ballot_count( (int) $existing['id'] ) > 0;
$error = $this->validate( $input, (bool) $locked );
if ( null !== $error ) {
$this->error = $error;
$this->old = $input;
return;
}
$fields = [
'title' => $this->title_or_question( $input ),
'question' => $input['question'],
'image_id' => $input['image_id'],
'status' => $input['status'],
] + ( $locked ? $this->rule_fields( $existing ) : $this->rule_fields( $this->stored_rules( $input ) ) );
if ( ! $existing ) {
try {
$fields['code'] = $this->repo->next_code();
} catch ( \RuntimeException $e ) {
$this->error = __( 'Could not make a poll code. Save again.', 'polenta' );
$this->old = $input;
return;
}
$id = $this->repo->insert_poll( $fields );
if ( ! $this->repo->replace_choices( $id, $this->choice_rows( $input ) ) ) {
$this->repo->delete_poll( $id );
$this->error = __( 'That poll did not save. Try again.', 'polenta' );
$this->old = $input;
return;
}
wp_safe_redirect( admin_url( 'admin.php?page=poller-edit&poll=' . $id . '&poller_notice=saved' ) );
exit;
}
$id = (int) $existing['id'];
$saved = $this->repo->save_poll( $id, $fields, $locked ? null : $this->choice_rows( $input ) );
$notice = ( ! $locked && ! $saved ) ? 'kept' : 'saved';
wp_safe_redirect( admin_url( 'admin.php?page=poller-edit&poll=' . $id . '&poller_notice=' . $notice ) );
exit;
}
public function handle_delete(): void {
$id = absint( $_POST['poll_id'] ?? 0 );
check_admin_referer( 'poller_delete_poll_' . $id );
$this->guard();
if ( $this->repo->get_poll( $id ) ) {
$this->repo->delete_poll( $id );
}
wp_safe_redirect( admin_url( 'admin.php?page=poller&poller_notice=deleted' ) );
exit;
}
public function handle_status(): void {
$id = absint( $_POST['poll_id'] ?? 0 );
check_admin_referer( 'poller_status_' . $id );
$this->guard();
$status = ( isset( $_POST['status'] ) && 'closed' === $_POST['status'] ) ? 'closed' : 'open';
if ( $this->repo->get_poll( $id ) ) {
$this->repo->set_status( $id, $status );
}
$notice = 'closed' === $status ? 'closed' : 'opened';
wp_safe_redirect( admin_url( 'admin.php?page=poller&poller_notice=' . $notice ) );
exit;
}
/**
* Copy a poll's setup and choices into a new closed poll, then open the
* copy's edit screen.
*/
public function handle_duplicate(): void {
$id = absint( $_POST['poll_id'] ?? 0 );
check_admin_referer( 'poller_duplicate_poll_' . $id );
$this->guard();
$copy = $this->repo->duplicate_poll( $id );
if ( null === $copy ) {
wp_safe_redirect( admin_url( 'admin.php?page=poller&poller_notice=not_duplicated' ) );
exit;
}
wp_safe_redirect( admin_url( 'admin.php?page=poller-edit&poll=' . $copy . '&poller_notice=duplicated' ) );
exit;
}
/**
* Start a new round on a poll. The live tally starts empty and earlier
* rounds stay under Past rounds. Posted from the poll list or the edit
* screen, and goes back to the page it came from.
*/
public function handle_reset(): void {
$id = absint( $_POST['poll_id'] ?? 0 );
check_admin_referer( 'poller_reset_poll_' . $id );
$this->guard();
if ( ! $this->repo->reset_poll( $id ) ) {
wp_safe_redirect( admin_url( 'admin.php?page=poller' ) );
exit;
}
$back = wp_get_referer();
if ( ! $back ) {
$back = admin_url( 'admin.php?page=poller' );
}
wp_safe_redirect( add_query_arg( 'poller_notice', 'reset', remove_query_arg( 'poller_notice', $back ) ) );
exit;
}
/**
* Hide a word cloud entry, or show it again. Posted from the poll's edit
* screen or from the results on the public poll page.
*/
public function handle_hide_word(): void {
$id = absint( $_POST['poll_id'] ?? 0 );
check_admin_referer( 'poller_hide_word_' . $id );
$this->guard();
$poll = $this->repo->get_poll( $id );
// BallotRules::word() cleans the text into the key it was counted under.
// sanitize_text_field() would turn "<3" into "<3" and miss that key.
$word = isset( $_POST['word'] ) ? BallotRules::word( wp_unslash( $_POST['word'] ) ) : null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$hide = isset( $_POST['hide'] ) && sanitize_key( wp_unslash( $_POST['hide'] ) ) === '1';
if ( $poll && 'words' === $poll['selection'] && null !== $word ) {
$this->repo->set_word_hidden( $id, $word['key'], $hide );
}
$back = wp_get_referer();
if ( ! $back ) {
$back = $this->edit_url( $id );
}
wp_safe_redirect( add_query_arg( 'poller_notice', $hide ? 'hidden' : 'shown', remove_query_arg( 'poller_notice', $back ) ) );
exit;
}
public function render_list(): void {
$this->guard();
$polls = $this->repo->list_polls();
?>
notices(); ?>
|
|
|
|
|
reset_form( (int) $poll['id'] ); ?>
|
selection_label( $poll ) );
?>
local_time( (string) $poll['created_at'] ) ); ?>
guard();
if ( $this->old ) {
$input = $this->old;
$poll = $input['id'] ? $this->repo->get_poll( (int) $input['id'] ) : null;
} else {
$id = isset( $_GET['poll'] ) ? absint( wp_unslash( $_GET['poll'] ) ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$poll = $id ? $this->repo->get_poll( $id ) : null;
if ( $id && ! $poll ) {
wp_die( esc_html__( 'That poll does not exist.', 'polenta' ), '', [ 'response' => 404 ] );
}
$input = $poll ? $this->input_from_poll( $poll ) : $this->blank_input();
}
$locked = $poll && $this->repo->ballot_count( (int) $poll['id'] ) > 0;
$choices = $input['choices'];
if ( ! $locked ) {
while ( count( $choices ) < 2 ) {
$choices[] = [
'label' => '',
'image_id' => 0,
];
}
}
$prompt = $this->image_preview( (int) $input['image_id'] );
?>
notices(); ?>
error ) : ?>
human_join()
)
);
?>
reset_form( (int) $poll['id'], 'poller-reset-form' );
}
?>
render_words( $poll ); ?>
render_past_rounds( $poll );
}
?>
>
' . esc_html( $label ) . '';
}
private function guard(): void {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'You do not have permission to manage polls.', 'polenta' ), '', [ 'response' => 403 ] );
}
}
private function notices(): void {
// Read-only status from an admin redirect. The value is matched against a fixed list.
if ( ! isset( $_GET['poller_notice'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
return;
}
$key = sanitize_key( wp_unslash( $_GET['poller_notice'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$map = [
'saved' => __( 'Poll saved.', 'polenta' ),
'deleted' => __( 'Poll deleted.', 'polenta' ),
'closed' => __( 'Poll closed.', 'polenta' ),
'opened' => __( 'Poll opened.', 'polenta' ),
'duplicated' => __( 'Poll duplicated.', 'polenta' ),
'reset' => __( 'New round started. Earlier results are kept under Past rounds on the poll.', 'polenta' ),
'not_duplicated' => __( 'That poll could not be duplicated. Try again.', 'polenta' ),
'kept' => __( 'People voted while you were editing, so the choices were kept.', 'polenta' ),
'hidden' => __( 'Word hidden.', 'polenta' ),
'shown' => __( 'Word shown again.', 'polenta' ),
];
if ( ! isset( $map[ $key ] ) ) {
return;
}
$class = in_array( $key, [ 'kept', 'not_duplicated' ], true ) ? 'notice-warning' : 'notice-success';
echo '' . esc_html( $map[ $key ] ) . '
';
}
/**
* @return array
*/
private function posted_poll(): array {
check_admin_referer( 'poller_save_poll' );
$labels = ( isset( $_POST['choice_label'] ) && is_array( $_POST['choice_label'] ) )
? map_deep( wp_unslash( $_POST['choice_label'] ), 'sanitize_text_field' )
: [];
$images = ( isset( $_POST['choice_image'] ) && is_array( $_POST['choice_image'] ) )
? map_deep( wp_unslash( $_POST['choice_image'] ), 'absint' )
: [];
$count = max( count( $labels ), count( $images ) );
$choices = [];
for ( $i = 0; $i < $count; $i++ ) {
$label_raw = $labels[ $i ] ?? '';
$image_raw = $images[ $i ] ?? 0;
if ( ! is_scalar( $label_raw ) || ! is_scalar( $image_raw ) ) {
continue;
}
$label = sanitize_text_field( (string) $label_raw );
$image_id = absint( $image_raw );
if ( '' === $label && 0 === $image_id ) {
continue;
}
$choices[] = [
'label' => $label,
'image_id' => $image_id,
];
}
$pasted = isset( $_POST['choice_list'] ) ? sanitize_textarea_field( wp_unslash( $_POST['choice_list'] ) ) : '';
$lines = preg_split( '/\R/', $pasted );
if ( false === $lines ) {
$lines = [];
}
foreach ( $lines as $line ) {
$label = sanitize_text_field( $line );
if ( '' !== $label ) {
$choices[] = [
'label' => $label,
'image_id' => 0,
];
}
}
$kind = ( isset( $_POST['kind'] ) && sanitize_key( wp_unslash( $_POST['kind'] ) ) === 'image' ) ? 'image' : 'text';
$selection = isset( $_POST['selection'] ) ? sanitize_key( wp_unslash( $_POST['selection'] ) ) : '';
if ( ! in_array( $selection, [ 'single', 'multi', 'dots', 'words' ], true ) ) {
$selection = 'single';
}
$budget = isset( $_POST['budget'] ) ? absint( wp_unslash( $_POST['budget'] ) ) : BallotRules::DEFAULT_BUDGET;
$stacking = ! empty( $_POST['stacking'] );
$per_choice = isset( $_POST['per_choice'] ) ? absint( wp_unslash( $_POST['per_choice'] ) ) : $budget;
$word_limit = isset( $_POST['word_limit'] ) ? absint( wp_unslash( $_POST['word_limit'] ) ) : BallotRules::DEFAULT_WORDS;
$status = ( isset( $_POST['status'] ) && sanitize_key( wp_unslash( $_POST['status'] ) ) === 'closed' ) ? 'closed' : 'open';
return [
'id' => isset( $_POST['poll_id'] ) ? absint( wp_unslash( $_POST['poll_id'] ) ) : 0,
'title' => isset( $_POST['title'] ) ? sanitize_text_field( wp_unslash( $_POST['title'] ) ) : '',
'question' => isset( $_POST['question'] ) ? sanitize_textarea_field( wp_unslash( $_POST['question'] ) ) : '',
'image_id' => isset( $_POST['image_id'] ) ? absint( wp_unslash( $_POST['image_id'] ) ) : 0,
'kind' => $kind,
'selection' => $selection,
'budget' => $budget,
'stacking' => $stacking,
'per_choice' => $per_choice,
'spend_all' => ! empty( $_POST['spend_all'] ),
'word_limit' => $word_limit,
'status' => $status,
'choices' => $choices,
];
}
/**
* @param array $input
*/
private function validate( array $input, bool $locked ): ?string {
if ( '' === $input['question'] ) {
return __( 'Write the question people will see.', 'polenta' );
}
if ( $this->length( $input['question'] ) > 500 ) {
return __( 'Keep the question under 500 characters.', 'polenta' );
}
if ( $this->length( $input['title'] ) > 160 ) {
return __( 'Keep the name under 160 characters.', 'polenta' );
}
if ( $input['image_id'] && ! wp_attachment_is_image( $input['image_id'] ) ) {
return __( 'The question picture needs to be an image.', 'polenta' );
}
if ( $locked ) {
return null;
}
if ( 'words' === $input['selection'] ) {
$limit = (int) $input['word_limit'];
if ( $limit < 1 || $limit > BallotRules::MAX_WORDS ) {
return sprintf(
/* translators: %d is the most words a person can enter. */
__( 'Let each person enter from 1 to %d words.', 'polenta' ),
BallotRules::MAX_WORDS
);
}
return null;
}
if ( count( $input['choices'] ) < 2 ) {
return __( 'Add at least two choices.', 'polenta' );
}
$max = BallotRules::max_choices( $input['selection'] );
if ( count( $input['choices'] ) > $max ) {
return sprintf(
/* translators: %d is the most choices a poll can have. */
__( 'A poll like this can have up to %d choices.', 'polenta' ),
$max
);
}
if ( 'dots' === $input['selection'] ) {
$error = $this->validate_dots( $input );
if ( null !== $error ) {
return $error;
}
}
foreach ( $input['choices'] as $choice ) {
if ( $this->length( $choice['label'] ) > 160 ) {
return __( 'Keep each choice under 160 characters.', 'polenta' );
}
if ( 'text' === $input['kind'] && '' === $choice['label'] ) {
return __( 'Each choice needs a label.', 'polenta' );
}
if ( 'image' === $input['kind'] && ( ! $choice['image_id'] || ! wp_attachment_is_image( $choice['image_id'] ) ) ) {
return __( 'Each picture choice needs an image.', 'polenta' );
}
}
return null;
}
/**
* @param array $input
*/
private function validate_dots( array $input ): ?string {
$budget = (int) $input['budget'];
if ( $budget < 1 || $budget > BallotRules::MAX_BUDGET ) {
return sprintf(
/* translators: %d is the most dots a person can have. */
__( 'Give each person from 1 to %d dots.', 'polenta' ),
BallotRules::MAX_BUDGET
);
}
$rules = $this->stored_rules( $input );
if ( $input['stacking'] && $budget > 1 && ( $input['per_choice'] < 2 || $input['per_choice'] > $budget ) ) {
return sprintf(
/* translators: %d is how many dots each person has. */
__( 'Set the most dots on one item from 2 to %d.', 'polenta' ),
$budget
);
}
if ( $rules['spend_all'] && count( $input['choices'] ) * $rules['per_choice'] < $budget ) {
return __( 'There is no way to use every dot with these items. Add items, allow more dots on one item, or turn off "People must use every dot".', 'polenta' );
}
return null;
}
/**
* Voting rules as they are stored. With stacking off, an item takes 1 dot.
* A word cloud keeps its words per person in budget. Other polls store zeros.
*
* @param array $input
* @return array
*/
private function stored_rules( array $input ): array {
$rules = [
'kind' => $input['kind'],
'selection' => $input['selection'],
'budget' => 0,
'per_choice' => 0,
'spend_all' => false,
];
if ( 'words' === $input['selection'] ) {
$rules['kind'] = 'text';
$rules['budget'] = (int) $input['word_limit'];
return $rules;
}
if ( 'dots' !== $input['selection'] ) {
return $rules;
}
$budget = (int) $input['budget'];
$rules['budget'] = $budget;
$rules['per_choice'] = $input['stacking'] ? max( 1, min( (int) $input['per_choice'], $budget ) ) : 1;
$rules['spend_all'] = (bool) $input['spend_all'];
return $rules;
}
/**
* @param array $poll
* @return array
*/
private function rule_fields( array $poll ): array {
return [
'kind' => (string) $poll['kind'],
'selection' => (string) $poll['selection'],
'budget' => (int) $poll['budget'],
'per_choice' => (int) $poll['per_choice'],
'spend_all' => ! empty( $poll['spend_all'] ),
];
}
/**
* @param array $poll
*/
private function selection_label( array $poll ): string {
if ( 'multi' === $poll['selection'] ) {
return __( 'more than one', 'polenta' );
}
if ( 'words' === $poll['selection'] ) {
$limit = (int) $poll['budget'];
return sprintf(
/* translators: %d is how many words each person can enter. */
_n( 'word cloud, %d word each', 'word cloud, up to %d words each', $limit, 'polenta' ),
$limit
);
}
if ( 'dots' !== $poll['selection'] ) {
return __( 'one', 'polenta' );
}
$budget = (int) $poll['budget'];
$label = sprintf(
/* translators: %d is how many dots each person has. */
_n( 'dot voting, %d dot', 'dot voting, %d dots', $budget, 'polenta' ),
$budget
);
if ( (int) $poll['per_choice'] > 1 ) {
$label .= ', ' . sprintf(
/* translators: %d is the most dots on one item. */
__( 'up to %d on one item', 'polenta' ),
(int) $poll['per_choice']
);
}
if ( ! empty( $poll['spend_all'] ) ) {
$label .= ', ' . __( 'use every dot', 'polenta' );
}
return $label;
}
/**
* @param array $input
* @return array
*/
private function choice_rows( array $input ): array {
$rows = [];
if ( 'words' === $input['selection'] ) {
return $rows;
}
foreach ( $input['choices'] as $choice ) {
$rows[] = [
'label' => $choice['label'],
'image_id' => 'image' === $input['kind'] ? (int) $choice['image_id'] : 0,
];
}
return $rows;
}
/**
* @param array $input
*/
private function title_or_question( array $input ): string {
if ( '' !== $input['title'] ) {
return $input['title'];
}
$flat = trim( (string) preg_replace( '/\s+/', ' ', $input['question'] ) );
if ( function_exists( 'mb_substr' ) ) {
return mb_substr( $flat, 0, 80 );
}
return substr( $flat, 0, 80 );
}
/**
* @param array $poll
* @return array
*/
private function input_from_poll( array $poll ): array {
$choices = [];
foreach ( $this->repo->get_choices( (int) $poll['id'] ) as $choice ) {
$choices[] = [
'label' => $choice['label'],
'image_id' => $choice['image_id'],
];
}
$dots = 'dots' === $poll['selection'];
return [
'id' => (int) $poll['id'],
'title' => (string) $poll['title'],
'question' => (string) $poll['question'],
'image_id' => (int) $poll['image_id'],
'kind' => (string) $poll['kind'],
'selection' => (string) $poll['selection'],
'budget' => $dots && (int) $poll['budget'] > 0 ? (int) $poll['budget'] : BallotRules::DEFAULT_BUDGET,
'stacking' => (int) $poll['per_choice'] > 1,
'per_choice' => (int) $poll['per_choice'] > 1 ? (int) $poll['per_choice'] : max( 2, $dots ? (int) $poll['budget'] : BallotRules::DEFAULT_BUDGET ),
'spend_all' => ! empty( $poll['spend_all'] ),
'word_limit' => 'words' === $poll['selection'] && (int) $poll['budget'] > 0 ? (int) $poll['budget'] : BallotRules::DEFAULT_WORDS,
'status' => (string) $poll['status'],
'choices' => $choices,
];
}
/**
* @return array
*/
private function blank_input(): array {
return [
'id' => 0,
'title' => '',
'question' => '',
'image_id' => 0,
'kind' => 'text',
'selection' => 'single',
'budget' => BallotRules::DEFAULT_BUDGET,
'stacking' => false,
'per_choice' => BallotRules::DEFAULT_BUDGET,
'spend_all' => false,
'word_limit' => BallotRules::DEFAULT_WORDS,
'status' => 'open',
'choices' => [],
];
}
/**
* Every word entered on a word cloud poll, with a Hide or Show button.
* Rendered after the poll form, since each button is its own small form.
*
* @param array $poll
*/
private function render_words( array $poll ): void {
$id = (int) $poll['id'];
$hidden = $this->repo->hidden_words( $id );
$words = Tally::words( $this->repo->word_rows( $id ), [], 0 );
if ( ! $words && ! $hidden ) {
echo '' . esc_html__( 'No words yet.', 'polenta' ) . '
';
return;
}
$is_hidden = array_fill_keys( $hidden, true );
$seen = [];
echo '' . esc_html__( 'Hide a word to take it off the room display and the results. People who entered it still see it in their own answers.', 'polenta' ) . '
';
echo '' . esc_html__( 'Word', 'polenta' ) . ' ' . esc_html__( 'People', 'polenta' ) . ' ' . esc_html__( 'Action', 'polenta' ) . ' ';
foreach ( $words as $word ) {
$seen[ $word['key'] ] = true;
$this->word_row( $id, $word['text'], (int) $word['count'], isset( $is_hidden[ $word['key'] ] ) );
}
foreach ( $hidden as $key ) {
if ( ! isset( $seen[ $key ] ) ) {
$this->word_row( $id, $key, 0, true );
}
}
echo '
';
}
private function word_row( int $poll_id, string $text, int $count, bool $hidden ): void {
?>
>
$poll
*/
private function render_past_rounds( array $poll ): void {
$id = (int) $poll['id'];
$rounds = $this->repo->past_rounds( $id );
if ( ! $rounds ) {
return;
}
echo '' . esc_html__( 'Past rounds', 'polenta' ) . ' ';
echo '' . esc_html__( 'Reset votes starts a new round. The answers from earlier rounds are kept here.', 'polenta' ) . '
';
foreach ( $rounds as $round ) {
$number = (int) $round['round'];
$summary = sprintf(
/* translators: 1: round number, 2: how many people answered, 3: first answer time, 4: last answer time. */
_n( 'Round %1$d: %2$d person answered, %3$s to %4$s', 'Round %1$d: %2$d people answered, %3$s to %4$s', (int) $round['ballots'], 'polenta' ),
$number,
(int) $round['ballots'],
$this->local_time( $round['first_at'] ),
$this->local_time( $round['last_at'] )
);
echo '' . esc_html( $summary ) . ' ';
$this->render_round_results( $id, $number );
echo ' ';
}
}
/**
* One round's results. The voting rules can change between rounds, so a
* round with words reads as a word list and any other as choice totals.
*/
private function render_round_results( int $poll_id, int $round ): void {
$rows = $this->repo->word_rows( $poll_id, $round );
if ( $rows ) {
$is_hidden = array_fill_keys( $this->repo->hidden_words( $poll_id, $round ), true );
echo '' . esc_html__( 'Word', 'polenta' ) . ' ' . esc_html__( 'People', 'polenta' ) . ' ';
foreach ( Tally::words( $rows, [], 0 ) as $word ) {
$hidden = isset( $is_hidden[ $word['key'] ] );
echo '' . esc_html( $word['text'] );
if ( $hidden ) {
echo ' ' . esc_html__( '(hidden)', 'polenta' ) . ' ';
}
echo ' ' . esc_html( (string) $word['count'] ) . ' ';
}
echo '
';
return;
}
$counts = $this->repo->choice_counts( $poll_id, $round );
$choices = [];
$index = 0;
foreach ( $this->repo->round_choices( $poll_id, $round ) as $choice ) {
$index++;
$label = (string) $choice['label'];
if ( '' === $label ) {
$label = sprintf(
/* translators: %d is the picture number on the ballot. */
__( 'Picture %d', 'polenta' ),
$index
);
}
$choices[] = [
'label' => $label,
'image_id' => (int) $choice['image_id'],
'votes' => (int) ( $counts[ (int) $choice['id'] ] ?? 0 ),
];
}
echo '' . esc_html__( 'Choice', 'polenta' ) . ' ' . esc_html__( 'Votes', 'polenta' ) . ' ';
foreach ( Tally::rank( $choices ) as $choice ) {
$thumb = $this->image_preview( $choice['image_id'] );
echo '';
if ( '' !== $thumb ) {
echo ' ';
}
echo esc_html( $choice['label'] ) . ' ' . esc_html( (string) $choice['votes'] ) . ' ';
}
echo '
';
}
private function image_preview( int $id ): string {
if ( $id <= 0 ) {
return '';
}
$url = wp_get_attachment_image_url( $id, 'thumbnail' );
return $url ? $url : '';
}
private function edit_url( int $id ): string {
return admin_url( 'admin.php?page=poller-edit&poll=' . $id );
}
private function local_time( string $gmt ): string {
return get_date_from_gmt( $gmt, get_option( 'date_format' ) . ' ' . get_option( 'time_format' ) );
}
private function human_join(): string {
$human = preg_replace( '#^https?://#', '', untrailingslashit( Plugin::join_url() ) );
return $human ? $human : Plugin::join_url();
}
private function length( string $text ): int {
return function_exists( 'mb_strlen' ) ? mb_strlen( $text ) : strlen( $text );
}
}