• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

equalizedigital / accessibility-checker / 16152724110

08 Jul 2025 07:37PM UTC coverage: 29.405%. Remained the same
16152724110

push

github

web-flow
Merge pull request #1042 from equalizedigital/steve/pro-182-if-there-are-no-issues-then-average-issues-per-page-should

update: average issues per post calculation to handle zero issues case

0 of 1 new or added line in 1 file covered. (0.0%)

1 existing line in 1 file now uncovered.

1572 of 5346 relevant lines covered (29.41%)

1.8 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

59.92
/admin/class-scans-stats.php
1
<?php
2
/**
3
 * Class file for scans stats
4
 *
5
 * @package Accessibility_Checker
6
 */
7

8
namespace EDAC\Admin;
9

10
use EqualizeDigital\AccessibilityCheckerPro\Admin\Scans;
11

12
/**
13
 * Class that handles calculating scans stats
14
 */
15
class Scans_Stats {
16

17
        /**
18
         * Number of seconds to return results from cache.
19
         *
20
         * @var integer
21
         */
22
        private $cache_time = 0;
23

24
        /**
25
         * Max number of issues to consider for results.
26
         *
27
         * @var integer
28
         */
29
        private $record_limit = 100000;
30

31
        /**
32
         * Total number of rules that are run on each page during a scan
33
         *
34
         * @var integer
35
         */
36
        private $rule_count;
37

38
        /**
39
         * Prefix used cache name
40
         *
41
         * @var string
42
         */
43
        private $cache_name_prefix;
44

45
        /**
46
         * Constructor
47
         *
48
         * @param integer $cache_time number of seconds to return the results from cache.
49
         */
50
        public function __construct( $cache_time = 60 * 60 * 24 ) {
51

52
                $this->cache_time        = $cache_time;
8✔
53
                $this->cache_name_prefix = 'edac_scans_stats_' . EDAC_VERSION . '_' . $this->record_limit;
8✔
54
                $this->rule_count        = count( edac_register_rules() );
8✔
55
        }
56

57
        /**
58
         * Load all stats into the cache. Should be called by a background scheduled.
59
         *
60
         * @return void
61
         */
62
        public function load_cache() {
63

64
                // Cache the summary.
65
                $this->summary();
×
66

67
                // Cache the post_types.
68
                $scannable_post_types = Settings::get_scannable_post_types();
×
69

70
                $post_types = get_post_types(
×
71
                        [
×
72
                                'public' => true,
×
73
                        ]
×
74
                );
×
75

76
                unset( $post_types['attachment'] );
×
77

78
                foreach ( $post_types as $post_type ) {
×
79
                        if ( in_array( $post_type, $scannable_post_types, true ) ) {
×
80
                                $this->issues_summary_by_post_type( $post_type );
×
81
                        }
82
                }
83
        }
84

85
        /**
86
         * Clear the summary and post type scans stats that have been cached
87
         *
88
         * @return void
89
         */
90
        public function clear_cache() {
91

92
                // Delete the cached summary stats.
93
                $transient_name = $this->cache_name_prefix . '_summary';
8✔
94
                delete_transient( $transient_name );
8✔
95

96
                // Delete the cached post_type stats.
97
                $post_types = get_post_types(
8✔
98
                        [
8✔
99
                                'public' => true,
8✔
100
                        ]
8✔
101
                );
8✔
102
                unset( $post_types['attachment'] );
8✔
103
                foreach ( $post_types as $post_type ) {
8✔
104
                        $transient_name = $this->cache_name_prefix . '_issues_summary_by_post_type_' . $post_type;
8✔
105
                        delete_transient( $transient_name );
8✔
106
                }
107
        }
108

109
        /**
110
         * Gets summary information about all scans
111
         *
112
         * @param  boolean $skip_cache whether to skip the cache.
113
         * @return array
114
         */
115
        public function summary( $skip_cache = false ) {
116

117
                global $wpdb;
8✔
118

119
                $transient_name = $this->cache_name_prefix . '_summary';
8✔
120

121
                $cache = get_transient( $transient_name );
8✔
122

123
                if ( ! $skip_cache && ( $this->cache_time && $cache ) ) {
8✔
124

125
                        if ( $cache['expires_at'] >= time() && $cache['cached_at'] + $this->cache_time >= time()
×
126
                        ) {
127

128
                                $full_scan_completed_at = (int) get_option( 'edacp_fullscan_completed_at' );
×
129
                                if ( $full_scan_completed_at <= $cache['cached_at'] ) {
×
130
                                        // There hasn't been a full scan completed since we've cached, so return these results.
131
                                        $cache['cache_hit'] = true;
×
132
                                        return $cache;
×
133
                                }
134
                        }
135
                }
136

137
                $data = [];
8✔
138

139
                $scannable_posts_count = Settings::get_scannable_posts_count();
8✔
140
                $tests_count           = $scannable_posts_count * $this->rule_count;
8✔
141
                $siteid                = get_current_blog_id();
8✔
142

143
                $data['scannable_posts_count']      = (int) $scannable_posts_count;
8✔
144
                $data['rule_count']                 = (int) $this->rule_count;
8✔
145
                $data['tests_count']                = (int) $tests_count;
8✔
146
                $data['scannable_post_types_count'] = (int) count( Settings::get_scannable_post_types() );
8✔
147

148
                $post_types = get_post_types(
8✔
149
                        [
8✔
150
                                'public' => true,
8✔
151
                        ]
8✔
152
                );
8✔
153
                unset( $post_types['attachment'] );
8✔
154

155
                $data['public_post_types_count'] = (int) count( $post_types );
8✔
156

157
                $issues_query = new Issues_Query( [], $this->record_limit, Issues_Query::FLAG_INCLUDE_ALL_POST_TYPES );
8✔
158

159
                // Count total unique meta values from postmeta table where the meta_key is _edac_issue_density.
160
                // This will give us the total number of posts that have been scanned.
161
                $data['posts_scanned'] = (int) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Using direct query for adding data to database, caching not required for one time operation.
8✔
162
                        $wpdb->prepare(
8✔
163
                                'SELECT COUNT(DISTINCT %i) FROM %i WHERE meta_key = %s',
8✔
164
                                'post_id',
8✔
165
                                $wpdb->postmeta,
8✔
166
                                '_edac_issue_density'
8✔
167
                        )
8✔
168
                );
8✔
169

170

171
                $data['is_truncated']      = $issues_query->has_truncated_results();
8✔
172
                $data['posts_with_issues'] = (int) $issues_query->distinct_posts_count();
8✔
173
                $data['rules_failed']      = 0;
8✔
174

175
                // Get a count of rules that are not in the issues table.
176
                $rule_slugs = array_map(
8✔
177
                        function ( $item ) {
8✔
178
                                return $item['slug'];
8✔
179
                        },
8✔
180
                        edac_register_rules()
8✔
181
                );
8✔
182

183
                foreach ( $rule_slugs as $rule_slug ) {
8✔
184
                        $rule_query = new Issues_Query(
8✔
185
                                [
8✔
186
                                        'rule_slugs' => [ $rule_slug ],
8✔
187
                                ],
8✔
188
                                1,
8✔
189
                                Issues_Query::FLAG_INCLUDE_ALL_POST_TYPES
8✔
190
                        );
8✔
191

192
                        if ( $rule_query->count() ) {
8✔
193
                                        ++$data['rules_failed'];
2✔
194
                        }
195
                }
196
                $data['rules_passed'] = $this->rule_count - $data['rules_failed'];
8✔
197

198
                $data['passed_percentage'] = 100;
8✔
199
                if ( $data['posts_scanned'] > 0 && $tests_count > 0 ) {
8✔
200
                        $data['passed_percentage'] = round( ( $data['rules_passed'] / $this->rule_count ) * 100, 2 );
×
201
                }
202

203
                $warning_issues_query      = new Issues_Query(
8✔
204
                        [
8✔
205
                                'post_types' => Settings::get_scannable_post_types(),
8✔
206
                                'rule_types' => [ Issues_Query::RULETYPE_WARNING ],
8✔
207
                        ],
8✔
208
                        $this->record_limit
8✔
209
                );
8✔
210
                $data['warnings']          = (int) $warning_issues_query->count();
8✔
211
                $data['distinct_warnings'] = (int) $warning_issues_query->distinct_count();
8✔
212

213
                $contrast_issues_query = new Issues_Query(
8✔
214
                        [
8✔
215
                                'post_types' => Settings::get_scannable_post_types(),
8✔
216
                                'rule_types' => [ Issues_Query::RULETYPE_COLOR_CONTRAST ],
8✔
217
                        ],
8✔
218
                        $this->record_limit
8✔
219
                );
8✔
220

221
                $data['contrast_errors']          = (int) $contrast_issues_query->count();
8✔
222
                $data['distinct_contrast_errors'] = (int) $contrast_issues_query->distinct_count();
8✔
223

224
                $error_issues_query      = new Issues_Query(
8✔
225
                        [
8✔
226
                                'post_types' => Settings::get_scannable_post_types(),
8✔
227
                                'rule_types' => [ Issues_Query::RULETYPE_ERROR ],
8✔
228
                        ],
8✔
229
                        $this->record_limit
8✔
230
                );
8✔
231
                $data['errors']          = (int) $error_issues_query->count();
8✔
232
                $data['distinct_errors'] = (int) $error_issues_query->distinct_count();
8✔
233

234
                $data['errors_without_contrast']          = $data['errors'] - $data['contrast_errors'];
8✔
235
                $data['distinct_errors_without_contrast'] = $data['distinct_errors'] - $data['distinct_contrast_errors'];
8✔
236

237
                $ignored_issues_query         = new Issues_Query(
8✔
238
                        [
8✔
239
                                'post_types' => Settings::get_scannable_post_types(),
8✔
240
                        ],
8✔
241
                        $this->record_limit,
8✔
242
                        Issues_Query::FLAG_ONLY_IGNORED
8✔
243
                );
8✔
244
                $data['ignored']              = (int) $ignored_issues_query->count();
8✔
245
                $data['distinct_ignored']     = (int) $ignored_issues_query->distinct_count();
8✔
246
                $data['posts_without_issues'] = 0;
8✔
247
                $data['avg_issues_per_post']  = 0;
8✔
248

249
                if ( $data['posts_scanned'] > 0
8✔
250
                        && ! empty( Settings::get_scannable_post_types() )
8✔
251
                        && ! empty( Settings::get_scannable_post_statuses() )
8✔
252
                ) {
253
                        $ac_table_name = $wpdb->prefix . 'accessibility_checker';
×
254

255
                        $posts_without_issues = "
×
256
                                SELECT COUNT({$wpdb->posts}.ID) FROM {$wpdb->posts}
×
257
                                LEFT JOIN " . $wpdb->prefix . "accessibility_checker ON {$wpdb->posts}.ID = " .
×
258
                                $wpdb->prefix . 'accessibility_checker.postid WHERE ' .
×
259
                                $wpdb->prefix . 'accessibility_checker.postid IS NULL
×
260
                                AND post_type IN(' .
×
261
                                        Helpers::array_to_sql_safe_list(
×
262
                                                Settings::get_scannable_post_types()
×
263
                                        ) . ')
×
264
                                AND post_status IN(' .
×
265
                                        Helpers::array_to_sql_safe_list(
×
266
                                                Settings::get_scannable_post_statuses()
×
267
                                        ) . ')';
×
268

269
                        // give me sql that will find all post ids in the accessibility_checker table
270
                        // where ALL issues with that ID are eiter ignored or globally ignored.
271
                        $posts_with_just_ignored_issues = $wpdb->prepare(
×
272
                                'SELECT COUNT( DISTINCT postid )
×
273
                                FROM %i
274
                                WHERE siteid = %d
275
                                AND postid NOT IN (
276
                                          SELECT postid
277
                                          FROM %i
278
                                          WHERE ignre=0 AND ignre_global=0
279
                                )',
×
280
                                [ $ac_table_name, $siteid, $ac_table_name ]
×
281
                        );
×
282

283
                        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Using direct query for adding data to database, caching not required for one time operation.
284
                        $data['posts_without_issues'] = $wpdb->get_var( $posts_without_issues ) + $wpdb->get_var( $posts_with_just_ignored_issues );
×
285
                        $data['avg_issues_per_post']  = round( ( $data['warnings'] + $data['errors'] ) / $data['posts_scanned'], 2 );
×
286

287
                        // Show "< 1" only when there are issues but the average is less than 1 (excluding exactly 0).
NEW
288
                        if ( $data['avg_issues_per_post'] < 1 && $data['avg_issues_per_post'] > 0 ) {
×
UNCOV
289
                                $data['avg_issues_per_post'] = '< 1';
×
290
                        }
291
                }
292

293
                // Average issue density percentage is the sum of all post densities divided by the number of posts scanned.
294
                $data['avg_issue_density_percentage'] =
8✔
295
                        $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Using direct query for adding data to database, caching not required for one time operation.
8✔
296
                                $wpdb->prepare(
8✔
297
                                        'SELECT AVG(meta_value)
8✔
298
                                        FROM ' . $wpdb->postmeta . '
8✔
299
                                        JOIN (
300
                                                SELECT DISTINCT postid
301
                                                FROM ' . $wpdb->prefix . 'accessibility_checker
8✔
302
                                        ) AS distinct_posts ON ' . $wpdb->postmeta . '.post_id = distinct_posts.postid
8✔
303
                                        WHERE meta_key = %s',
8✔
304
                                        [ '_edac_issue_density' ]
8✔
305
                                )
8✔
306
                        );
8✔
307

308
                if ( null === $data['avg_issue_density_percentage'] ) {
8✔
309
                        $data['avg_issue_density_percentage'] = 'N/A';
8✔
310
                } else {
311
                        $data['avg_issue_density_percentage'] = round( $data['avg_issue_density_percentage'], 2 );
×
312
                }
313

314
                $data['fullscan_running']      = false;
8✔
315
                $data['fullscan_state']        = '';
8✔
316
                $data['fullscan_completed_at'] = 0;
8✔
317

318
                // For back compat reasons the old class_exists is kept and moved to an else block.
319
                // After a few releases the else should be removed.
320
                if ( class_exists( '\EqualizeDigital\AccessibilityCheckerPro\Admin\Scans' ) ) {
8✔
321
                        $scans      = new Scans();
×
322
                        $scan_state = $scans->scan_state();
×
323

324
                        $data['fullscan_state'] = $scan_state;
×
325
                        if ( Scans::SCAN_STATE_PHP_SCAN_RUNNING === $scan_state
×
326
                                || Scans::SCAN_STATE_JS_SCAN_RUNNING === $scan_state
×
327
                        ) {
328
                                $data['fullscan_running'] = true;
×
329
                        }
330
                        $data['fullscan_completed_at'] = $scans->scan_date( 'php' );
×
331
                } elseif ( class_exists( '\EDACP\Scans' ) ) {
8✔
332
                        $scans      = new \EDACP\Scans();
×
333
                        $scan_state = $scans->scan_state();
×
334

335
                        $data['fullscan_state'] = $scan_state;
×
336
                        if ( \EDACP\Scans::SCAN_STATE_PHP_SCAN_RUNNING === $scan_state
×
337
                                || \EDACP\Scans::SCAN_STATE_JS_SCAN_RUNNING === $scan_state
×
338
                        ) {
339
                                $data['fullscan_running'] = true;
×
340
                        }
341
                        $data['fullscan_completed_at'] = $scans->scan_date( 'php' );
×
342

343
                }
344

345
                $data['cache_id']   = $transient_name;
8✔
346
                $data['cached_at']  = time();
8✔
347
                $data['expires_at'] = time() + $this->cache_time;
8✔
348
                $data['cache_hit']  = false;
8✔
349

350
                // Handle formatting. Assumes all are numbers except for those listed in exceptions.
351
                $formatting            = [];
8✔
352
                $formatting_exceptions = [
8✔
353
                        'is_truncated',
8✔
354
                        'passed_percentage',
8✔
355
                        'avg_issue_density_percentage',
8✔
356
                        'fullscan_running',
8✔
357
                        'fullscan_state',
8✔
358
                        'fullscan_completed_at',
8✔
359
                        'cache_id',
8✔
360
                        'cached_at',
8✔
361
                        'expires_at',
8✔
362
                        'cache_hit',
8✔
363
                ];
8✔
364

365
                foreach ( $data as $key => $value ) {
8✔
366
                        if ( ! in_array( $key, $formatting_exceptions, true ) ) {
8✔
367
                                $formatting[ $key . '_formatted' ] = Helpers::format_number( $value );
8✔
368
                        }
369
                }
370

371
                // Handle exceptions.
372
                $formatting['fullscan_completed_at_formatted'] = 'N/A';
8✔
373
                if ( $data['fullscan_completed_at'] > 0 ) {
8✔
374
                        $formatting['fullscan_completed_at_formatted'] = Helpers::format_date( $data['fullscan_completed_at'], true );
×
375
                }
376
                $formatting['passed_percentage_formatted'] = Helpers::format_percentage( $data['passed_percentage'] );
8✔
377

378
                // The density should already a percentage value, just add the sign. If not an integer, just return the value which will be 'N/A'.
379
                $formatting['avg_issue_density_percentage_formatted'] = is_int( $data['avg_issue_density_percentage'] ) ? $data['avg_issue_density_percentage'] . '%' : $data['avg_issue_density_percentage'];
8✔
380

381
                $formatting['cached_at_formatted'] = Helpers::format_date( $data['cached_at'], true );
8✔
382

383
                $data = array_merge( $data, $formatting );
8✔
384

385
                if ( $data['posts_scanned'] > 0 ) {
8✔
386
                        set_transient( $transient_name, $data, $this->cache_time );
×
387
                } else {
388
                        // no posts have been scanned, so clear any previously cache results.
389
                        $this->clear_cache();
8✔
390
                }
391

392
                return $data;
8✔
393
        }
394

395
        /**
396
         * Gets issues summary information about a post type
397
         *
398
         * @param  string $post_type post type.
399
         * @return array .
400
         */
401
        public function issues_summary_by_post_type( $post_type ) {
402

403
                $transient_name = $this->cache_name_prefix . '_issues_summary_by_post_type_' . $post_type;
×
404

405
                $cache = get_transient( $transient_name );
×
406

407
                if ( $this->cache_time && $cache ) {
×
408

409
                        if ( $cache['expires_at'] >= time() && $cache['cached_at'] + $this->cache_time >= time()
×
410
                        ) {
411

412
                                $full_scan_completed_at = (int) get_option( 'edacp_fullscan_completed_at' );
×
413
                                if ( $full_scan_completed_at <= $cache['cached_at'] ) {
×
414
                                        // There hasn't been a full scan completed since we've cached, so return these results.
415
                                        $cache['cache_hit'] = true;
×
416
                                        return $cache;
×
417
                                }
418
                        }
419
                }
420

421
                $data = [];
×
422

423
                $error_issues_query      = new Issues_Query(
×
424
                        [
×
425
                                'rule_types' => [ Issues_Query::RULETYPE_ERROR ],
×
426
                                'post_types' => [ $post_type ],
×
427
                        ],
×
428
                        $this->record_limit
×
429
                );
×
430
                $data['errors']          = $error_issues_query->count();
×
431
                $data['distinct_errors'] = $error_issues_query->distinct_count();
×
432

433
                $warning_issues_query      = new Issues_Query(
×
434
                        [
×
435
                                'rule_types' => [ Issues_Query::RULETYPE_WARNING ],
×
436
                                'post_types' => [ $post_type ],
×
437
                        ],
×
438
                        $this->record_limit
×
439
                );
×
440
                $data['warnings']          = $warning_issues_query->count();
×
441
                $data['distinct_warnings'] = $warning_issues_query->distinct_count();
×
442

443
                $color_contrast_issues_query      = new Issues_Query(
×
444
                        [
×
445
                                'rule_types' => [ Issues_Query::RULETYPE_COLOR_CONTRAST ],
×
446
                                'post_types' => [ $post_type ],
×
447
                        ],
×
448
                        $this->record_limit
×
449
                );
×
450
                $data['contrast_errors']          = $color_contrast_issues_query->count();
×
451
                $data['distinct_contrast_errors'] = $color_contrast_issues_query->distinct_count();
×
452

453
                $data['errors_without_contrast']          = $data['errors'] - $data['contrast_errors'];
×
454
                $data['distinct_errors_without_contrast'] = $data['distinct_errors'] - $data['distinct_contrast_errors'];
×
455

456
                foreach ( $data as $key => $val ) {
×
457
                        $data[ $key . '_formatted' ] = Helpers::format_number( $val );
×
458
                }
459

460
                $data['cache_id']   = $transient_name;
×
461
                $data['cached_at']  = time();
×
462
                $data['expires_at'] = time() + $this->cache_time;
×
463
                $data['cache_hit']  = false;
×
464

465
                set_transient( $transient_name, $data, $this->cache_time );
×
466

467
                return $data;
×
468
        }
469
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc