LH Polls
Simple polls for LocalHero. Options and votes in custom tables, optional comments via wp_comments.
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/lhero-org/lh-polls/archive/refs/heads/main.zipSimple member polls for LocalHero. Options and votes stored in custom tables, votes linked to WordPress comments for UI integration, surfaced via a Gutenberg block.
Description
LH Polls provides a WhatsApp-style polling experience for logged-in members. Each poll supports either single-choice or multiple-choice voting. Votes are backed by WordPress comments (enabling use of the existing comments UI and admin screens) with a dedicated votes table providing DB-level uniqueness constraints and fast querying. All read queries are cached via transients backed by the persistent object cache.
Polls are private custom post types — they have no front-end URL of their own and are embedded in pages and posts via the Poll block.
Requirements
- WordPress 6.6+
- PHP 8.0+
- BuddyPress (optional, not required)
- WP Statuses plugin (optional — enhances the publish metabox dropdown with Closed status)
Installation
- Upload the
lh-pollsdirectory towp-content/plugins/ - Activate the plugin (or network-activate on Multisite)
- Database tables are created automatically on activation and on first load if missing
Usage
Creating a poll
- Go to Polls → Add New in the admin menu
- Give the poll a title
- In the Poll Settings metabox: check Allow multiple choices if voters should be able to select more than one option
- In the Options metabox: add options one at a time using the New option field, clicking Update Post after each
- When ready, set status to Published to open voting
Embedding a poll
Insert the Poll block in any post or page. Select the poll from the dropdown in the block settings panel or inline. The block renders server-side — vote counts are always live.
Editing a poll from the front end
If the current logged-in user has the edit_post capability for a poll, a small Edit poll link is shown beneath the vote total on the front end. This links directly to the poll's edit screen in the admin dashboard.
Closing a poll
Set the poll's status to Closed using the Status dropdown in the publish metabox. Voting stops immediately. Results remain visible to all users.
Managing votes
The Votes metabox on the poll edit screen shows all votes grouped by option, with voter name, comment text, and date. Individual votes can be deleted via the Delete link. Votes can also be added manually by selecting an option and user and clicking Update Post.
Vote comments are standard WordPress comments of type lh_vote attached to the poll post. They can also be managed via the WordPress comments screen — deleting a comment there will automatically remove the corresponding vote row.
Capabilities
Poll management uses standard WordPress post capabilities (edit_posts, publish_posts, delete_posts etc.). Any user role that can manage posts can manage polls — no custom capability registration is needed.
Frontend voting requires only that the user be logged in.
Poll Statuses
| Status | Meaning |
|---|---|
draft |
Poll is being set up. Not visible in any block. |
publish |
Poll is live and accepting votes. |
lh_poll_closed |
Voting has ended. Results still visible. |
private |
Visible to admins only — useful for previewing before publishing. |
A poll requires at least two options before it can be published or closed. Attempting to publish with fewer than two options will force the status back to draft with an admin notice.
Database Tables
{prefix}lh_poll_options
Stores the options for each poll. Per-site on Multisite (uses $wpdb->prefix).
| Column | Type | Description |
|---|---|---|
option_id |
BIGINT UNSIGNED | Primary key |
poll_id |
BIGINT UNSIGNED | Parent poll post ID |
label |
VARCHAR(255) | Display label |
menu_order |
INT | Sort order |
{prefix}lh_poll_votes
Records each vote cast. Per-site on Multisite.
| Column | Type | Description |
|---|---|---|
vote_id |
BIGINT UNSIGNED | Primary key |
poll_id |
BIGINT UNSIGNED | Parent poll post ID |
option_id |
BIGINT UNSIGNED | Option voted for |
user_id |
BIGINT UNSIGNED | Voter |
comment_id |
BIGINT UNSIGNED | Linked wp_comments row |
voted_at |
DATETIME | Vote timestamp |
Unique constraint: (option_id, user_id) — one vote per user per option, enforced at the database level.
Comment Integration
Every vote creates a wp_comments row of type lh_vote attached to the poll post (comment_post_ID = poll_id). This enables:
- Optional comment text left alongside a vote
- Viewing and deleting votes via the standard WP comments admin screen
- Vote counts visible as
comment_counton the poll post
Vote comments are excluded from all standard comment queries, feeds, and counts on non-poll posts. Deleting a vote comment via the WP comments screen automatically removes the corresponding vote row via the deleted_comment hook.
Caching
All poll read queries are cached as transients backed by the persistent object cache (Redis on LocalHero). Cache is keyed per poll and automatically flushed on any vote insert, delete, or option change via LH_Polls_DB::flush_poll_cache().
Cached queries:
- Poll options list
- Option IDs
- Vote counts per option
- Per-option vote list with comment data
- Per-user voted option IDs
- Per-user poll-level voted flag
- Per-user per-option voted flag
Cache TTL is DAY_IN_SECONDS. Since invalidation is explicit on every write, the TTL is a safety net only.
Cache Invalidation (Page Cache)
After a vote is cast or changed, the plugin fires do_action('litespeed_purge_url', $page_url) to purge the embedding page from LiteSpeed's page cache. On non-LiteSpeed environments this action is a no-op.
Front-End Voting Flow
Voting uses a plain HTML form POST back to the current page — no AJAX, no backend admin URLs. The form is handled by template_redirect in LH_Polls_Block. After processing, the user is redirected back to the same page (with a #lh-poll-{id} fragment anchor) with a ?lh_poll_msg parameter that triggers an inline feedback message.
For single-choice polls, a Change vote button is shown to users who have already voted, allowing them to select a different option.
Multisite
Tables are per-site, using $wpdb->prefix. On network activation, tables are created for all existing sites. New sites added after activation get their tables via the wpmu_new_blog hook. The lh_polls_db_version option is stored per-site.
Constants
| Constant | Description |
|---|---|
LH_POLLS_VERSION |
Plugin version |
LH_POLLS_FILE |
Absolute path to main plugin file |
LH_POLLS_DIR |
Plugin directory path |
LH_POLLS_URL |
Plugin directory URL |
Hooks
No public hooks are exposed yet. Planned for a future release.
File Structure
lh-polls/
├── lh-polls.php # Main plugin file, bootstrap
├── readme.md # This file
├── index.php # Directory silence file
├── includes/
│ ├── class-lh-polls-db.php # All DB queries, table creation, caching
│ ├── class-lh-polls-post-types.php # CPT, metaboxes, admin handlers, validation
│ ├── class-lh-polls-comments.php # Comment/vote sync, comment filtering
│ └── class-lh-polls-block.php # Block registration, SSR, front-end form handler
└── blocks/lh-polls/
├── editor.js # Block editor JS (poll picker)
├── admin.js # Admin JS (Closed status in publish metabox)
├── style.css # Front-end styles
└── editor.css # Editor styles
Changelog
1.0.5 — 2026-06-04
- Added front-end edit link for users with
edit_postcapability on the poll
1.0.4 — 2026-06-04
- Switched
capability_typetopost— polls now use standard post capabilities (edit_postsetc.), no custom capability registration needed - Replaced
manage_optionschecks in admin handlers withedit_posts
1.0.3 — 2026-06-04
- Added minimum two options validation — polls cannot be published or closed with fewer than two options; attempting to do so forces status back to draft with an admin notice
1.0.2 — 2026-06-04
- Added
post_type => array('lh_poll')toregister_post_statusfor WP Statuses plugin compatibility - Updated admin JS to target
#wp-statuses-dropdown(WP Statuses) with Chosen refresh, falling back to#post_status(standard WP)
1.0.1 — 2026-06-04
- Replaced inline
wp_add_inline_scriptwith proper registeredadmin.jsandwp_localize_scriptfor the Closed status dropdown - Fixed
get_the_ID()returning 0 on admin screens — now reads post ID from$_GET['post'] - Added
DOMContentLoadedguard to admin script
1.0.0 — 2026-06-04
- Initial release
lh_pollprivate CPT with options in{prefix}lh_poll_optionscustom table- Votes in
{prefix}lh_poll_voteswithUNIQUE KEY (option_id, user_id)DB-level constraint - Every vote backed by a
wp_commentsrow of typelh_vote; deleting the comment removes the vote row - Single-choice and multiple-choice modes controlled by
_poll_allow_multiplepost meta lh_poll_closedcustom post status for ended polls; open/closed determined bypost_status = publish- Poll block with server-side render; form POST back to current page via
template_redirect - Change vote support for single-choice polls
- Admin votes metabox with per-option breakdown, manual vote add/delete
- Full transient caching of all read queries, flushed on any write via
flush_poll_cache() - LiteSpeed page cache purge via
do_action('litespeed_purge_url')after votes - Multisite support: per-site tables,
wpmu_new_bloghook, network activation loop - WP Statuses plugin compatibility for publish metabox dropdown