| @@ -7,9 +7,8 @@ | ||
| 7 | 7 | */ |
| 8 | 8 | |
| 9 | 9 | namespace SRFM\Inc; |
| 10 | 10 | |
| 11 | -use SRFM\Inc\Database\Tables\Entries; | |
| 12 | 11 | use SRFM\Inc\Traits\Get_Instance; |
| 13 | 12 | use WP_Error; |
| 14 | 13 | use WP_REST_Response; |
| 15 | 14 | |
| @@ -58,9 +57,9 @@ | ||
| 58 | 57 | * @return true|WP_Error True if the request has read access, WP_Error object otherwise. |
| 59 | 58 | * @since 0.0.1 |
| 60 | 59 | */ |
| 61 | 60 | public function get_form_permissions_check() { |
| 62 | - if ( Helper::current_user_can( 'edit_posts' ) ) { | |
| 61 | + if ( current_user_can( 'edit_posts' ) ) { | |
| 63 | 62 | return true; |
| 64 | 63 | } |
| 65 | 64 | |
| 66 | 65 | return new \WP_Error( |
| @@ -109,459 +108,6 @@ | ||
| 109 | 108 | ]; |
| 110 | 109 | } |
| 111 | 110 | |
| 112 | 111 | return new WP_REST_Response( $data ); |
| 113 | - } | |
| 114 | - | |
| 115 | - /** | |
| 116 | - * Get forms list for the forms listing page. | |
| 117 | - * | |
| 118 | - * @param \WP_REST_Request $request Full details about the request. | |
| 119 | - * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. | |
| 120 | - * @since 2.0.0 | |
| 121 | - */ | |
| 122 | - public function get_forms_list( $request ) { | |
| 123 | - $nonce = sanitize_text_field( Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) ) ); | |
| 124 | - | |
| 125 | - Helper::verify_nonce_and_capabilities( 'rest', $nonce, 'wp_rest' ); | |
| 126 | - | |
| 127 | - // Get and validate request parameters. | |
| 128 | - $page = max( 1, Helper::get_integer_value( $request->get_param( 'page' ) ) ); | |
| 129 | - $status = sanitize_text_field( $request->get_param( 'status' ) ); | |
| 130 | - | |
| 131 | - // Get per_page from option first, then request parameter, with fallback to 10. | |
| 132 | - $saved_per_page = Helper::get_srfm_option( 'forms_per_page', 10 ); | |
| 133 | - $request_per_page = $request->get_param( 'per_page' ); | |
| 134 | - $per_page = $request_per_page ? min( 100, max( 1, Helper::get_integer_value( $request_per_page ) ) ) : $saved_per_page; | |
| 135 | - | |
| 136 | - // Save per_page to option if it came from request. | |
| 137 | - if ( $request_per_page && 'trash' !== $status && 1 < $request_per_page ) { | |
| 138 | - Helper::update_srfm_option( 'forms_per_page', $per_page ); | |
| 139 | - } | |
| 140 | - | |
| 141 | - $search = sanitize_text_field( $request->get_param( 'search' ) ); | |
| 142 | - $orderby = sanitize_text_field( $request->get_param( 'orderby' ) ); | |
| 143 | - $order = sanitize_text_field( $request->get_param( 'order' ) ); | |
| 144 | - $date_from = sanitize_text_field( $request->get_param( 'after' ) ); | |
| 145 | - $date_to = sanitize_text_field( $request->get_param( 'before' ) ); | |
| 146 | - | |
| 147 | - // Build query arguments. | |
| 148 | - $args = [ | |
| 149 | - 'post_type' => SRFM_FORMS_POST_TYPE, | |
| 150 | - 'post_status' => 'any' === $status ? [ 'publish', 'draft' ] : $status, | |
| 151 | - 'posts_per_page' => $per_page, | |
| 152 | - 'paged' => $page, | |
| 153 | - 'orderby' => $orderby, | |
| 154 | - 'order' => $order, | |
| 155 | - ]; | |
| 156 | - | |
| 157 | - // Add date range filtering. | |
| 158 | - if ( ! empty( $date_from ) || ! empty( $date_to ) ) { | |
| 159 | - $date_query = []; | |
| 160 | - // Handle 'after' date. | |
| 161 | - if ( ! empty( $date_from ) ) { | |
| 162 | - $date_query['after'] = $date_from; | |
| 163 | - } | |
| 164 | - | |
| 165 | - // Handle 'before' date - add 1 day to include the full end date. | |
| 166 | - if ( ! empty( $date_to ) ) { | |
| 167 | - $end_date = new \DateTime( $date_to ); | |
| 168 | - $end_date->add( new \DateInterval( 'P1D' ) ); // Add 1 day. | |
| 169 | - $date_query['before'] = $end_date->format( 'Y-m-d' ); | |
| 170 | - } | |
| 171 | - | |
| 172 | - $date_query['inclusive'] = true; | |
| 173 | - $args['date_query'] = [ $date_query ]; | |
| 174 | - } | |
| 175 | - | |
| 176 | - // Add search parameter. | |
| 177 | - if ( ! empty( $search ) ) { | |
| 178 | - if ( is_numeric( $search ) ) { | |
| 179 | - // Numeric search: match by form ID and title (e.g., "2024 Survey"). | |
| 180 | - $numeric_search = $search; | |
| 181 | - $where_filter = static function ( $where, $query ) use ( $numeric_search ) { | |
| 182 | - if ( ! $query->get( 'srfm_numeric_search' ) ) { | |
| 183 | - return $where; | |
| 184 | - } | |
| 185 | - global $wpdb; | |
| 186 | - $where .= $wpdb->prepare( | |
| 187 | - " AND ({$wpdb->posts}.ID = %d OR {$wpdb->posts}.post_title LIKE %s)", | |
| 188 | - absint( $numeric_search ), | |
| 189 | - '%' . $wpdb->esc_like( $numeric_search ) . '%' | |
| 190 | - ); | |
| 191 | - return $where; | |
| 192 | - }; | |
| 193 | - | |
| 194 | - add_filter( 'posts_where', $where_filter, 10, 2 ); | |
| 195 | - $args['srfm_numeric_search'] = true; | |
| 196 | - } else { | |
| 197 | - // Text search: match by title only. | |
| 198 | - $args['s'] = $search; | |
| 199 | - $args['search_columns'] = [ 'post_title' ]; | |
| 200 | - } | |
| 201 | - } | |
| 202 | - | |
| 203 | - // Execute query — use try/finally to guarantee filter cleanup. | |
| 204 | - try { | |
| 205 | - // Derived metrics can't be sorted by WP_Query, so they take a | |
| 206 | - // compute-sort-paginate pass instead. Only while tracking is on: with the | |
| 207 | - // feature off the columns are hidden, and a stored or hand-crafted request | |
| 208 | - // would run that expensive pass to order rows nobody can see. It also | |
| 209 | - // returns null when the site has more forms than the pass will scan. | |
| 210 | - $response_data = null; | |
| 211 | - $is_metric_sort = in_array( $orderby, [ 'views', 'conversion_rate' ], true ); | |
| 212 | - | |
| 213 | - if ( $is_metric_sort && Form_Views::get_instance()->is_tracking_enabled() ) { | |
| 214 | - $response_data = $this->get_forms_sorted_by_metric( $args, $orderby, $order, $page, Helper::get_integer_value( $per_page ) ); | |
| 215 | - } | |
| 216 | - | |
| 217 | - // True only when the metric sort actually ran. It returns null and falls back | |
| 218 | - // to date order when the feature is off or the site is past the sort ceiling; | |
| 219 | - // the table needs to know so it does not leave an active sort arrow on a | |
| 220 | - // column whose order was silently ignored. | |
| 221 | - $metric_sort_applied = $is_metric_sort && null !== $response_data; | |
| 222 | - | |
| 223 | - if ( null === $response_data ) { | |
| 224 | - if ( $is_metric_sort ) { | |
| 225 | - // WP_Query would silently discard 'views'/'conversion_rate' and fall | |
| 226 | - // back to post_date anyway. Say so explicitly so the behaviour is in | |
| 227 | - // the code rather than in core's tolerance for unknown keys. | |
| 228 | - $args['orderby'] = 'date'; | |
| 229 | - } | |
| 230 | - | |
| 231 | - $query = new \WP_Query( $args ); | |
| 232 | - | |
| 233 | - $forms = []; | |
| 234 | - /** | |
| 235 | - * Post object from the query. | |
| 236 | - * | |
| 237 | - * @var \WP_Post $post */ | |
| 238 | - foreach ( $query->posts as $post ) { | |
| 239 | - $forms[] = $this->prepare_form_for_listing( $post ); | |
| 240 | - } | |
| 241 | - | |
| 242 | - $response_data = [ | |
| 243 | - 'forms' => $forms, | |
| 244 | - 'total' => Helper::get_integer_value( $query->found_posts ), | |
| 245 | - 'total_pages' => Helper::get_integer_value( $query->max_num_pages ), | |
| 246 | - 'current_page' => $page, | |
| 247 | - 'per_page' => $per_page, | |
| 248 | - ]; | |
| 249 | - } | |
| 250 | - } finally { | |
| 251 | - if ( ! empty( $search ) && is_numeric( $search ) && isset( $where_filter ) ) { | |
| 252 | - remove_filter( 'posts_where', $where_filter, 10 ); | |
| 253 | - } | |
| 254 | - } | |
| 255 | - | |
| 256 | - // Travels with the rows so the table gates its columns on the same evaluation | |
| 257 | - // that produced them. The localized `srfm_admin` flag is only a page-load | |
| 258 | - // snapshot: toggle the setting in another tab and the open list would keep | |
| 259 | - // rendering columns while every row came back empty, which reads as data loss. | |
| 260 | - $response_data['views_enabled'] = Form_Views::get_instance()->is_tracking_enabled(); | |
| 261 | - | |
| 262 | - // Lets the table reset a stale metric sort arrow when the order silently fell | |
| 263 | - // back to date (feature off, or past the metric-sort ceiling). | |
| 264 | - $response_data['metric_sort_applied'] = $metric_sort_applied; | |
| 265 | - | |
| 266 | - return new WP_REST_Response( $response_data, 200 ); | |
| 267 | - } | |
| 268 | - | |
| 269 | - /** | |
| 270 | - * Sort the forms list by a derived metric (views or conversion rate) and paginate. | |
| 271 | - * | |
| 272 | - * WP_Query can't order by views (missing-meta forms would drop out) or by the | |
| 273 | - * derived conversion rate at all, so we fetch every matching form id, compute the | |
| 274 | - * metric, sort in PHP, then slice the requested page. Form counts are small in | |
| 275 | - * practice; revisit with a grouped query if a site accumulates thousands of forms. | |
| 276 | - * | |
| 277 | - * @param array<string,mixed> $args Base WP_Query args (filters/search), pagination ignored. | |
| 278 | - * @param string $orderby Either 'views' or 'conversion_rate'. | |
| 279 | - * @param string $order 'asc' or 'desc'. | |
| 280 | - * @param int $page Current page (1-based). | |
| 281 | - * @param int $per_page Items per page. | |
| 282 | - * @since 2.12.6 | |
| 283 | - * @return array<string,mixed>|null Response payload, or null when the site has more | |
| 284 | - * forms than this pass will scan and the caller | |
| 285 | - * should fall back to ordinary ordering. | |
| 286 | - */ | |
| 287 | - private function get_forms_sorted_by_metric( $args, $orderby, $order, $page, $per_page ) { | |
| 288 | - $id_args = $args; | |
| 289 | - $id_args['posts_per_page'] = -1; | |
| 290 | - $id_args['paged'] = 1; | |
| 291 | - $id_args['fields'] = 'ids'; | |
| 292 | - $id_args['orderby'] = 'ID'; | |
| 293 | - $id_args['order'] = 'DESC'; | |
| 294 | - | |
| 295 | - $id_query = new \WP_Query( $id_args ); | |
| 296 | - | |
| 297 | - /** | |
| 298 | - * Largest number of forms this path will sort before giving up. | |
| 299 | - * | |
| 300 | - * The pass is one entry COUNT per form, so cost grows linearly with the form | |
| 301 | - * count while only one page is ever displayed. Past this ceiling the request | |
| 302 | - * returns null so the caller falls back to ordinary date ordering rather than | |
| 303 | - * firing thousands of queries to render ten rows. | |
| 304 | - * | |
| 305 | - * @param int $limit Maximum forms to sort in PHP. Default 500. | |
| 306 | - * @since 2.12.6 | |
| 307 | - */ | |
| 308 | - $limit = Helper::get_integer_value( apply_filters( 'srfm_forms_metric_sort_limit', 500 ) ); | |
| 309 | - | |
| 310 | - if ( $limit > 0 && count( $id_query->posts ) > $limit ) { | |
| 311 | - /** | |
| 312 | - * Fires when the metric sort is skipped because the site has too many forms. | |
| 313 | - * | |
| 314 | - * Announced rather than skipped silently: a list that quietly ignores the | |
| 315 | - * column the user clicked reads as a broken sort, not as a deliberate | |
| 316 | - * ceiling. Gives a site owner something to hook if they hit it. | |
| 317 | - * | |
| 318 | - * @param string $orderby Requested metric, 'views' or 'conversion_rate'. | |
| 319 | - * @param int $count Number of forms that would have been sorted. | |
| 320 | - * @param int $limit The ceiling in force. | |
| 321 | - * @since 2.12.6 | |
| 322 | - */ | |
| 323 | - do_action( 'srfm_forms_metric_sort_skipped', $orderby, count( $id_query->posts ), $limit ); | |
| 324 | - | |
| 325 | - return null; | |
| 326 | - } | |
| 327 | - | |
| 328 | - // `fields => ids` skips the post-meta cache priming that a normal WP_Query does, | |
| 329 | - // so prime it once for all matched forms — otherwise each get_views() below is a | |
| 330 | - // separate get_post_meta() query (N+1). Entry counts are still one COUNT per form; | |
| 331 | - // acceptable for typical form volumes, revisit with a grouped query if needed. | |
| 332 | - if ( ! empty( $id_query->posts ) ) { | |
| 333 | - $prime_ids = array_map( | |
| 334 | - static function ( $post ) { | |
| 335 | - return (int) ( $post instanceof \WP_Post ? $post->ID : $post ); | |
| 336 | - }, | |
| 337 | - $id_query->posts | |
| 338 | - ); | |
| 339 | - update_meta_cache( 'post', $prime_ids ); | |
| 340 | - | |
| 341 | - // `fields => ids` skips the post cache, so the get_post() in the pagination | |
| 342 | - // loop below would be one query per displayed row. Prime it here instead. | |
| 343 | - // Terms and meta are handled separately, hence both flags false. | |
| 344 | - _prime_post_caches( $prime_ids, false, false ); | |
| 345 | - } | |
| 346 | - | |
| 347 | - $rows = []; | |
| 348 | - foreach ( $id_query->posts as $post_id ) { | |
| 349 | - $form_id = Helper::get_integer_value( $post_id ); | |
| 350 | - | |
| 351 | - // Same three arguments the render path passes. Calling this with only the | |
| 352 | - // form ID left $post_date_gmt empty, so strtotime() returned false, the | |
| 353 | - // "form is younger than the window" shortcut could never be taken, and the | |
| 354 | - // windowed COUNT ran for every row — both a second query per form and, for | |
| 355 | - // any entry whose created_at predates the form's post_date (an import, a | |
| 356 | - // migration, a restored backup), a different number than the column shows. | |
| 357 | - // The comment below promises order and display can never disagree; passing | |
| 358 | - // different arguments here is what made them disagree. | |
| 359 | - $post = get_post( $form_id ); | |
| 360 | - $metrics = $this->calculate_form_metrics( | |
| 361 | - $form_id, | |
| 362 | - $post->post_date_gmt ?? '', | |
| 363 | - Helper::get_integer_value( Entries::get_total_entries_by_status( 'all', $form_id ) ) | |
| 364 | - ); | |
| 365 | - | |
| 366 | - // Same helper the column renders from, so the order always matches the | |
| 367 | - // numbers on screen. An unmeasurable rate sorts as -1 rather than 0, so | |
| 368 | - // the dash rows group below a genuine 0% instead of tying with it. | |
| 369 | - $metric = 'views' === $orderby | |
| 370 | - ? (float) $metrics['views'] | |
| 371 | - : ( null === $metrics['conversion_rate'] ? -1.0 : (float) $metrics['conversion_rate'] ); | |
| 372 | - | |
| 373 | - $rows[] = [ | |
| 374 | - 'id' => $form_id, | |
| 375 | - 'metric' => $metric, | |
| 376 | - ]; | |
| 377 | - } | |
| 378 | - | |
| 379 | - // Sort by metric, tie-break on id (desc) for a stable order. | |
| 380 | - $direction = 'asc' === strtolower( $order ) ? 1 : -1; | |
| 381 | - usort( | |
| 382 | - $rows, | |
| 383 | - static function ( $a, $b ) use ( $direction ) { | |
| 384 | - if ( $a['metric'] === $b['metric'] ) { | |
| 385 | - return $b['id'] <=> $a['id']; | |
| 386 | - } | |
| 387 | - return ( $a['metric'] <=> $b['metric'] ) * $direction; | |
| 388 | - } | |
| 389 | - ); | |
| 390 | - | |
| 391 | - $total = count( $rows ); | |
| 392 | - $total_pages = $per_page > 0 ? (int) ceil( $total / $per_page ) : 1; | |
| 393 | - $offset = ( $page - 1 ) * $per_page; | |
| 394 | - $page_rows = array_slice( $rows, max( 0, $offset ), $per_page ); | |
| 395 | - | |
| 396 | - $forms = []; | |
| 397 | - foreach ( $page_rows as $row ) { | |
| 398 | - $post = get_post( $row['id'] ); | |
| 399 | - if ( $post instanceof \WP_Post ) { | |
| 400 | - $forms[] = $this->prepare_form_for_listing( $post ); | |
| 401 | - } | |
| 402 | - } | |
| 403 | - | |
| 404 | - return [ | |
| 405 | - 'forms' => $forms, | |
| 406 | - 'total' => $total, | |
| 407 | - 'total_pages' => $total_pages, | |
| 408 | - 'current_page' => $page, | |
| 409 | - 'per_page' => $per_page, | |
| 410 | - ]; | |
| 411 | - } | |
| 412 | - | |
| 413 | - /** | |
| 414 | - * The tracking-window boundary as a datetime string comparable to `created_at`. | |
| 415 | - * | |
| 416 | - * `created_at` is written by MySQL (`DEFAULT CURRENT_TIMESTAMP`) and compared in | |
| 417 | - * the session time zone, not UTC, so a bare `gmdate()` of a PHP timestamp is off | |
| 418 | - * by the MySQL/PHP clock offset — permanently mis-counting entries near the | |
| 419 | - * boundary. Entries::get_entries_count_after() solves this by reading | |
| 420 | - * `SELECT NOW()`, but it does so on every call, which would be one extra query | |
| 421 | - * per row on a listing page. The offset cannot change within a request, so it is | |
| 422 | - * resolved once and reused. | |
| 423 | - * | |
| 424 | - * @param int $window_start Unix timestamp. | |
| 425 | - * @return string Datetime string in MySQL's frame of reference. | |
| 426 | - * @since 2.12.6 | |
| 427 | - */ | |
| 428 | - private static function window_boundary_sql( $window_start ) { | |
| 429 | - static $offset_seconds = null; | |
| 430 | - | |
| 431 | - if ( null === $offset_seconds ) { | |
| 432 | - global $wpdb; | |
| 433 | - | |
| 434 | - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reading the server clock; a cached value would defeat the purpose. | |
| 435 | - $mysql_now = $wpdb->get_var( 'SELECT NOW()' ); | |
| 436 | - $mysql_ts = $mysql_now ? strtotime( (string) $mysql_now ) : false; | |
| 437 | - | |
| 438 | - // Fall back to no adjustment rather than a wild offset if NOW() is | |
| 439 | - // unreadable or unparseable — a UTC-configured server needs none anyway. | |
| 440 | - $offset_seconds = false === $mysql_ts ? 0 : $mysql_ts - time(); | |
| 441 | - } | |
| 442 | - | |
| 443 | - return gmdate( 'Y-m-d H:i:s', $window_start + $offset_seconds ); | |
| 444 | - } | |
| 445 | - | |
| 446 | - /** | |
| 447 | - * Views and conversion rate for one form. | |
| 448 | - * | |
| 449 | - * The single source of truth for both the rendered value and the sorted metric. | |
| 450 | - * They were computed separately at first, and drifted: the sort used all-time | |
| 451 | - * entries while the column used entries from the tracking window, so a form | |
| 452 | - * rendering a dash sorted as though its rate were several hundred percent. | |
| 453 | - * Anything needing these numbers must come through here. | |
| 454 | - * | |
| 455 | - * Returns `null` for the rate rather than a number whenever it cannot be | |
| 456 | - * measured — tracking off, window never opened, no views yet, or more entries | |
| 457 | - * than views. That last case is not possible in reality (every entry needs a | |
| 458 | - * view first), so it means the view count is incomplete and any percentage | |
| 459 | - * would be invented; the table renders the dash instead. `0.0` is reserved for | |
| 460 | - * a real measurement of zero. | |
| 461 | - * | |
| 462 | - * @param int $form_id Form post ID. | |
| 463 | - * @param string $post_date_gmt Form creation date, GMT. Used to skip a redundant count. | |
| 464 | - * @param int $entries_all_time All-time entry count, when the caller already has it. | |
| 465 | - * @return array{views:int,conversion_rate:float|null} | |
| 466 | - * @since 2.12.6 | |
| 467 | - */ | |
| 468 | - private function calculate_form_metrics( $form_id, $post_date_gmt = '', $entries_all_time = null ) { | |
| 469 | - $none = [ | |
| 470 | - 'views' => 0, | |
| 471 | - 'conversion_rate' => null, | |
| 472 | - ]; | |
| 473 | - | |
| 474 | - $window_start = Form_Views::get_instance()->get_tracking_started_at(); | |
| 475 | - | |
| 476 | - // A zero stamp means counting never started, so there is nothing to divide by | |
| 477 | - // and no window to measure against. Guarded as well as the display toggle | |
| 478 | - // because the two are written by different paths: the toggle could be forced | |
| 479 | - // on by a direct option write that never ran maybe_start_tracking(), and | |
| 480 | - // gmdate() on a zero timestamp would silently widen the window to 1970 and | |
| 481 | - // count every entry the form has ever had. | |
| 482 | - if ( ! Form_Views::get_instance()->is_tracking_enabled() || $window_start <= 0 ) { | |
| 483 | - return $none; | |
| 484 | - } | |
| 485 | - | |
| 486 | - $views = Form_Views::get_instance()->get_views( $form_id ); | |
| 487 | - | |
| 488 | - if ( $views <= 0 ) { | |
| 489 | - return $none; | |
| 490 | - } | |
| 491 | - | |
| 492 | - // Compare like with like. The Entries column is all-time, but views only start | |
| 493 | - // accruing when tracking opens, so the rate counts entries from that same | |
| 494 | - // moment — otherwise a form that existed beforehand divides years of entries by | |
| 495 | - // days of views and reports a rate that is pure noise. | |
| 496 | - $form_created = strtotime( (string) $post_date_gmt ); | |
| 497 | - | |
| 498 | - if ( null !== $entries_all_time && $form_created && $form_created >= $window_start ) { | |
| 499 | - // The form is younger than the window, so every entry it has is already | |
| 500 | - // inside the window and the caller's all-time count is the same number. | |
| 501 | - // Skips a second COUNT per row on the listing. | |
| 502 | - $entries_since = Helper::get_integer_value( $entries_all_time ); | |
| 503 | - } else { | |
| 504 | - $entries_since = Helper::get_integer_value( | |
| 505 | - Entries::get_total_entries_by_status( | |
| 506 | - 'all', | |
| 507 | - $form_id, | |
| 508 | - [ | |
| 509 | - [ | |
| 510 | - [ | |
| 511 | - 'key' => 'created_at', | |
| 512 | - 'compare' => '>=', | |
| 513 | - 'value' => self::window_boundary_sql( $window_start ), | |
| 514 | - ], | |
| 515 | - ], | |
| 516 | - ] | |
| 517 | - ) | |
| 518 | - ); | |
| 519 | - } | |
| 520 | - | |
| 521 | - if ( $entries_since > $views ) { | |
| 522 | - return [ | |
| 523 | - 'views' => $views, | |
| 524 | - 'conversion_rate' => null, | |
| 525 | - ]; | |
| 526 | - } | |
| 527 | - | |
| 528 | - return [ | |
| 529 | - 'views' => $views, | |
| 530 | - 'conversion_rate' => round( $entries_since / $views * 100, 1 ), | |
| 531 | - ]; | |
| 532 | - } | |
| 533 | - | |
| 534 | - /** | |
| 535 | - * Prepare a single form for the listing response. | |
| 536 | - * | |
| 537 | - * @param \WP_Post $post Post object. | |
| 538 | - * @return array<mixed> Prepared form data for listing. | |
| 539 | - * @since 2.0.0 | |
| 540 | - */ | |
| 541 | - private function prepare_form_for_listing( $post ) { | |
| 542 | - $form_id = $post->ID; | |
| 543 | - | |
| 544 | - // Get entries count. | |
| 545 | - $entries_count = Helper::get_integer_value( Entries::get_total_entries_by_status( 'all', $form_id ) ); | |
| 546 | - | |
| 547 | - // Views and conversion rate come from the same helper the sort path uses, so the | |
| 548 | - // column can never order by a different number than it displays. | |
| 549 | - $metrics = $this->calculate_form_metrics( $form_id, $post->post_date_gmt, $entries_count ); | |
| 550 | - $views = $metrics['views']; | |
| 551 | - $conversion_rate = $metrics['conversion_rate']; | |
| 552 | - | |
| 553 | - return [ | |
| 554 | - 'id' => $form_id, | |
| 555 | - 'title' => $post->post_title, | |
| 556 | - 'status' => $post->post_status, | |
| 557 | - 'date_created' => mysql_to_rfc3339( $post->post_date ), | |
| 558 | - 'date_modified' => mysql_to_rfc3339( $post->post_modified ), | |
| 559 | - 'entries_count' => $entries_count, | |
| 560 | - 'views' => $views, | |
| 561 | - 'conversion_rate' => $conversion_rate, | |
| 562 | - 'shortcode' => "[sureforms id='{$form_id}']", | |
| 563 | - 'edit_url' => admin_url( "post.php?post={$form_id}&action=edit" ), | |
| 564 | - 'frontend_url' => get_permalink( $form_id ), | |
| 565 | - ]; | |
| 566 | 112 | } |
| 567 | 113 | } |