From 7a6937b2f399ba3d274e742c50a17a525e117d74 Mon Sep 17 00:00:00 2001 From: Brian Foley Date: Tue, 8 Sep 2026 18:03:17 +0100 Subject: [PATCH 1/7] page_tally.inc: SitePageTally DTO for get_site_page_tally_summary --- pinc/page_tally.inc | 70 ++++++++++++++++++++++----------------------- 1 file changed, 34 insertions(+), 36 deletions(-) diff --git a/pinc/page_tally.inc b/pinc/page_tally.inc index 7656a84cd..feb56edb3 100644 --- a/pinc/page_tally.inc +++ b/pinc/page_tally.inc @@ -221,6 +221,19 @@ $SECONDS_TO_YESTERDAY = 1 + 3 * 60 * 60; // ----------------------------------------------------------------------------- +class SitePageTally +{ + public function __construct( + public readonly int $curr_day_goal, + public readonly int $prev_day_goal, + public readonly int $curr_month_goal, + public readonly int $curr_day_actual, + public readonly int $prev_day_actual, + public readonly int $curr_month_actual + ) { + } +} + /** * Return an object whose attributes are various useful statistics about * the site page tally. @@ -230,57 +243,42 @@ $SECONDS_TO_YESTERDAY = 1 + 3 * 60 * 60; * - prev_day_{goal,actual} * - curr_month_{goal,actual} */ -function get_site_page_tally_summary(string $tally_name): object +function get_site_page_tally_summary(string $tally_name): SitePageTally { - $site_stats = new StdClass(); - - // Goals + $tallyboard = new TallyBoard($tally_name, 'S'); + $holder_id = 1; + $current_tally = $tallyboard->get_current_tally($holder_id); + $snapshot_info = $tallyboard->get_info_from_latest_snapshot($holder_id); + $today = getdate(); + $curr_day_actual = $current_tally - $snapshot_info['tally_value']; - $site_stats->curr_day_goal = - get_site_tally_goal_summed( + return new SitePageTally( + // Goals + curr_day_goal: get_site_tally_goal_summed( $tally_name, "date = CURRENT_DATE" - ); - - $site_stats->prev_day_goal = - get_site_tally_goal_summed( + ), + prev_day_goal: get_site_tally_goal_summed( $tally_name, "date = (CURRENT_DATE - INTERVAL 1 DAY)" - ); - - $site_stats->curr_month_goal = - get_site_tally_goal_summed( + ), + curr_month_goal: get_site_tally_goal_summed( $tally_name, "date >= DATE_FORMAT(CURRENT_DATE, '%%Y-%%m-01') AND date <= LAST_DAY(CURRENT_DATE)" - ); + ), - // Actuals - - $tallyboard = new TallyBoard($tally_name, 'S'); - - $holder_id = 1; - - $current_tally = $tallyboard->get_current_tally($holder_id); - - $snapshot_info = $tallyboard->get_info_from_latest_snapshot($holder_id); - - $site_stats->curr_day_actual = - $current_tally - $snapshot_info['tally_value']; - - $site_stats->prev_day_actual = $snapshot_info['tally_delta']; - - $today = getdate(); - $site_stats->curr_month_actual = - $tallyboard->get_delta_sum( + // Actuals + curr_day_actual: $curr_day_actual, + prev_day_actual: $snapshot_info['tally_delta'], + curr_month_actual: $tallyboard->get_delta_sum( $holder_id, mktime(0, 0, 0, $today['mon'], 1, $today['year']), mktime(0, 0, 0, $today['mon'] + 1, 1, $today['year']) ) + - $site_stats->curr_day_actual; - - return $site_stats; + $curr_day_actual + ); } function get_site_tally_goal_summed(string $tally_name, string $date_condition): int From 51885c0a0825d63b55749a55e5773ee29e79a8a8 Mon Sep 17 00:00:00 2001 From: Brian Foley Date: Tue, 8 Sep 2026 09:47:01 +0100 Subject: [PATCH 2/7] Activity.inc: New UserAccess DTO --- pinc/Activity.inc | 148 ++++++++++++++++++++------------------- pinc/Stage.inc | 2 +- tools/proofers/round.php | 2 +- 3 files changed, 79 insertions(+), 73 deletions(-) diff --git a/pinc/Activity.inc b/pinc/Activity.inc index c3cf70dcd..1c437ed16 100644 --- a/pinc/Activity.inc +++ b/pinc/Activity.inc @@ -123,6 +123,29 @@ class Activities } } +class UserAccess +{ + /** + * @param bool $can_access + * user can access this activity. + * @param list{string, int, int, bool}[] $minima_table + * ( $criterion_str, $minimum, $user_score, $satisfied ) + * @param ?('sat-unneeded'|'sat-granted'|'sat-available'|'sat-requested'|'sat-wait'|'sat-denied'|'unsat-granted'|'unsat-requested'|'unsat-ungranted'|'unsat-denied') $request_status + * @param ?string $evaluation_criteria + * message to display about evaluation + */ + public function __construct( + public readonly ?string $username, + public readonly bool $can_access, + public readonly array $minima_table = [], + public readonly bool $all_minima_satisfied = false, + public readonly ?string $request_status = null, + public readonly ?string $evaluation_criteria = null, + public readonly ?string $activity_id = null + ) { + } +} + class Activity { /** @@ -137,7 +160,7 @@ class Activity * A (possibly empty) array of minimum requirements that a user must satisfy * in order to be allowed to participate in this activity * (barring special permission). - * @param string $after_satisfying_minima + * @param 'IMMEDIATE'|'REQ-AUTO'|'REQ-HUMAN'|'NOREQ' $after_satisfying_minima * After satisfying the above minima (if any), does the user have to do * anything else to work in this activity? * ``` @@ -199,36 +222,14 @@ class Activity } /** - * Return a User Access object - * - * Return an object with the following properties: - * - can_access: - * a boolean: TRUE iff the user can access this activity. - * - minima_table: - * an array of arrays (4-tuples): - * ( $criterion_str, $minimum, $user_score, $satisfied ) - * - all_minima_satisfied: - * boolean - * - request_status: - * enumerated type: sat-unneeded, sat-granted, sat-available, sat-requested, - * sat-wait, sat-denied - * unsat-granted, unsat-requested, unsat-ungranted, - * unsat-denied - * - evaluation_criteria: - * message to display about evaluation - * * UNIMPLEMENTED: * If $n_pages_completed is non-null, use it as the number of pages * that the user has completed. Otherwise, consult the database. */ - // TODO(jchaffraix): Add a class for the UserAccess object for type soundness. - public function user_access(?string $username, ?int $n_pages_completed = null): object + public function user_access(?string $username, ?int $n_pages_completed = null): UserAccess { if (is_null($username)) { - $uao = new StdClass(); // user access object - $uao->username = $username; - $uao->can_access = false; - return $uao; + return new UserAccess(username: null, can_access: false); } // A user's score isn't going to change in a page load but many page @@ -252,55 +253,52 @@ class Activity // ----------------------------------- - $uao = new StdClass(); // user access object - $uao->username = $username; - $uao->activity_id = $this->id; - $uao->evaluation_criteria = $this->evaluation_criteria; // Considering the minima... global $ACCESS_CRITERIA; - { - $uao->minima_table = []; - $uao->all_minima_satisfied = true; - foreach ($this->access_minima as $criterion_code => $minimum) { - $criterion_str = $ACCESS_CRITERIA[$criterion_code]; - $user_score = $user_scores[$criterion_code]; + $minima_table = []; + $all_minima_satisfied = true; + foreach ($this->access_minima as $criterion_code => $minimum) { + $criterion_str = $ACCESS_CRITERIA[$criterion_code]; + $user_score = $user_scores[$criterion_code]; - $satisfied = ($user_score >= $minimum); - $uao->minima_table[$criterion_code] = [$criterion_str, $minimum, $user_score, $satisfied]; - if (!$satisfied) { - $uao->all_minima_satisfied = false; - } + $satisfied = ($user_score >= $minimum); + $minima_table[$criterion_code] = [$criterion_str, $minimum, $user_score, $satisfied]; + if (!$satisfied) { + $all_minima_satisfied = false; } } + $can_access = false; + $requst_status = null; + if ($recorded_access == 'denied') { // they are not allowed to access this activity or to request access - $uao->can_access = false; - if ($uao->all_minima_satisfied) { - $uao->request_status = 'sat-denied'; + $can_access = false; + if ($all_minima_satisfied) { + $request_status = 'sat-denied'; } else { - $uao->request_status = 'unsat-denied'; + $request_status = 'unsat-denied'; } - } elseif ($uao->all_minima_satisfied) { + } elseif ($all_minima_satisfied) { // They've satisfied the requirements... switch ($this->after_satisfying_minima) { case 'IMMEDIATE': // They get immediate access. - $uao->request_status = 'sat-unneeded'; - $uao->can_access = true; + $request_status = 'sat-unneeded'; + $can_access = true; break; case 'REQ-AUTO': // They must ask for access, but it is auto-granted. if ($recorded_access == 'yes') { // They already have access - $uao->request_status = 'sat-granted'; - $uao->can_access = true; + $request_status = 'sat-granted'; + $can_access = true; } else { // They haven't asked yet, but can - $uao->request_status = 'sat-available'; - $uao->can_access = false; + $request_status = 'sat-available'; + $can_access = false; } break; @@ -308,16 +306,16 @@ class Activity // They must ask for access, and it must be human-granted. if ($recorded_access == 'yes') { // They already have access - $uao->request_status = 'sat-granted'; - $uao->can_access = true; + $request_status = 'sat-granted'; + $can_access = true; } elseif ($recorded_access == 'requested') { // They've asked, but it hasn't been granted yet - $uao->request_status = 'sat-requested'; - $uao->can_access = false; + $request_status = 'sat-requested'; + $can_access = false; } else { // They haven't asked yet, but can - $uao->request_status = 'sat-available'; - $uao->can_access = false; + $request_status = 'sat-available'; + $can_access = false; } break; @@ -327,12 +325,12 @@ class Activity // they are approved. if ($recorded_access == 'yes') { // They already have access - $uao->request_status = 'sat-granted'; - $uao->can_access = true; + $request_status = 'sat-granted'; + $can_access = true; } else { // They are waiting - $uao->request_status = 'sat-wait'; - $uao->can_access = false; + $request_status = 'sat-wait'; + $can_access = false; } break; @@ -343,24 +341,32 @@ class Activity // They don't satisfy the requirements if ($recorded_access == 'yes') { // but they already have access anyway - $uao->request_status = 'unsat-granted'; - $uao->can_access = true; + $request_status = 'unsat-granted'; + $can_access = true; } elseif ($recorded_access == 'requested') { // but they've requested access, but don't have it yet // This is unusual, but can happen. // E.g., at some point in the past, they satisfied the minima and // requested access, but now they no longer satisfy the minima // (quiz-pass could expire, page-tally could be reduced by clears). - $uao->request_status = 'unsat-requested'; - $uao->can_access = false; + $request_status = 'unsat-requested'; + $can_access = false; } else { // and they can't ask for access - $uao->request_status = 'unsat-ungranted'; - $uao->can_access = false; + $request_status = 'unsat-ungranted'; + $can_access = false; } } - return $uao; + return new UserAccess( + username: $username, + activity_id: $this->id, + evaluation_criteria: $this->evaluation_criteria, + minima_table: $minima_table, + all_minima_satisfied: $all_minima_satisfied, + can_access: $can_access, + request_status: $request_status + ); } } @@ -403,7 +409,7 @@ function get_user_score(User $user_obj, string $criterion_code): float // -------------------------------------------------------------------------- -function show_user_access_object(object $uao, bool $will_autogrant = false): void +function show_user_access_object(UserAccess $uao, bool $will_autogrant = false): void { global $code_url; @@ -427,7 +433,7 @@ function show_user_access_object(object $uao, bool $will_autogrant = false): voi echo ""; echo "$criterion_str"; echo "$minimum"; - td_w_bgcolor($user_score, $satisfied); + td_w_bgcolor((string)$user_score, $satisfied); echo "\n"; } echo "\n"; @@ -508,7 +514,7 @@ function show_user_access_object(object $uao, bool $will_autogrant = false): voi } } -function grant_user_access_if_sat(object &$uao): void +function grant_user_access_if_sat(UserAccess &$uao): void { $activity = Activities::get_by_id($uao->activity_id); if ($uao->request_status == 'sat-available' && diff --git a/pinc/Stage.inc b/pinc/Stage.inc index 8f746117c..b6ef2b014 100644 --- a/pinc/Stage.inc +++ b/pinc/Stage.inc @@ -64,7 +64,7 @@ class Stage extends Activity echo "

$title

\n" . get_page_header_image($this->id); } - public function page_top(object $uao): void + public function page_top(UserAccess $uao): void { $this->page_header("{$this->id}: {$this->name}"); diff --git a/tools/proofers/round.php b/tools/proofers/round.php index 0f0bbb9d6..d5dd7e08c 100644 --- a/tools/proofers/round.php +++ b/tools/proofers/round.php @@ -34,7 +34,7 @@ // show user how to access this round if (!$uao->can_access) { - show_user_access_object($uao, true /* will_autogrant */); + show_user_access_object($uao, will_autogrant: true); } encourage_highest_round($pguser, $round->id); From a5111083ffad72b6794d90e26ce879ea433bee47 Mon Sep 17 00:00:00 2001 From: Brian Foley Date: Tue, 8 Sep 2026 22:05:13 +0100 Subject: [PATCH 3/7] ProjectState.inc: Add ProjectStateDescriptor DTO --- pinc/ProjectState.inc | 79 +++++++++++++++++++++++++------------------ 1 file changed, 46 insertions(+), 33 deletions(-) diff --git a/pinc/ProjectState.inc b/pinc/ProjectState.inc index 27c2bc6f4..dfe2e8f0f 100644 --- a/pinc/ProjectState.inc +++ b/pinc/ProjectState.inc @@ -328,6 +328,19 @@ function get_project_status_descriptors(): array return ['posted', 'PPd', 'proofed', 'created']; } +class ProjectStatusDescriptor +{ + public function __construct( + public readonly string $state_selector, + public readonly string $Xed_title, + public readonly string $projects_Xed_title, + public readonly string $graphs_title, + public readonly string $per_day_title, + public readonly string $cumulative_title + ) { + } +} + /** * Return an object with details about a project status * @@ -340,20 +353,19 @@ function get_project_status_descriptors(): array * @param 'created'|'proofed'|'PPd'|'posted' $which * A word denoting a possible status of a project */ -function get_project_status_descriptor(string $which): object +function get_project_status_descriptor(string $which): ProjectStatusDescriptor { - $obj = new stdClass(); - switch ($which) { case 'created': $not_created_states = [PROJ_NEW, PROJ_DELETE]; - $obj->state_selector = "(state NOT IN (" . surround_and_join($not_created_states, "'", "'", ",") . "))"; - $obj->Xed_title = _('Created'); - $obj->projects_Xed_title = _('Projects Created'); - $obj->graphs_title = _('Created Projects Graphs'); - $obj->per_day_title = _('Projects Created Each Day'); - $obj->cumulative_title = _('Cumulative Projects Created'); - break; + return new ProjectStatusDescriptor( + state_selector: "(state NOT IN (" . surround_and_join($not_created_states, "'", "'", ",") . "))", + Xed_title: _('Created'), + projects_Xed_title: _('Projects Created'), + graphs_title: _('Created Projects Graphs'), + per_day_title: _('Projects Created Each Day'), + cumulative_title: _('Cumulative Projects Created') + ); case 'proofed': $proofed_states = [ @@ -365,13 +377,14 @@ function get_project_status_descriptor(string $which): object PROJ_SUBMIT_PG_POSTED, PROJ_POST_COMPLETE, ]; - $obj->state_selector = "(state IN (" . surround_and_join($proofed_states, "'", "'", ",") . "))"; - $obj->Xed_title = _('Proofread'); - $obj->projects_Xed_title = _('Projects Proofread'); - $obj->graphs_title = _('Proofread Projects Graphs'); - $obj->per_day_title = _('Projects Proofread Each Day'); - $obj->cumulative_title = _('Cumulative Projects Proofread'); - break; + return new ProjectStatusDescriptor( + state_selector: "(state IN (" . surround_and_join($proofed_states, "'", "'", ",") . "))", + Xed_title: _('Proofread'), + projects_Xed_title: _('Projects Proofread'), + graphs_title: _('Proofread Projects Graphs'), + per_day_title: _('Projects Proofread Each Day'), + cumulative_title: _('Cumulative Projects Proofread') + ); case 'PPd': $pped_states = [ @@ -379,26 +392,26 @@ function get_project_status_descriptor(string $which): object PROJ_POST_SECOND_CHECKED_OUT, PROJ_SUBMIT_PG_POSTED, ]; - $obj->state_selector = "(state IN (" . surround_and_join($pped_states, "'", "'", ",") . "))"; - $obj->Xed_title = _('Post-Processed'); - $obj->projects_Xed_title = _('Projects Post-Processed'); - $obj->graphs_title = _('PPd Projects Graphs'); - $obj->per_day_title = _('Projects Post-Processed Each Day'); - $obj->cumulative_title = _('Cumulative Projects PPd'); - break; + return new ProjectStatusDescriptor( + state_selector: "(state IN (" . surround_and_join($pped_states, "'", "'", ",") . "))", + Xed_title: _('Post-Processed'), + projects_Xed_title: _('Projects Post-Processed'), + graphs_title: _('PPd Projects Graphs'), + per_day_title: _('Projects Post-Processed Each Day'), + cumulative_title: _('Cumulative Projects PPd') + ); case 'posted': - $obj->state_selector = "(state = '" . PROJ_SUBMIT_PG_POSTED . "')"; - $obj->Xed_title = _('Posted to PG'); - $obj->projects_Xed_title = _('Projects Posted'); - $obj->graphs_title = _('Posted Projects Graphs'); - $obj->per_day_title = _('Projects Posted to PG Each Day'); - $obj->cumulative_title = _('Cumulative Projects Posted to PG'); - break; + return new ProjectStatusDescriptor( + state_selector: "(state = '" . PROJ_SUBMIT_PG_POSTED . "')", + Xed_title: _('Posted to PG'), + projects_Xed_title: _('Projects Posted'), + graphs_title: _('Posted Projects Graphs'), + per_day_title: _('Projects Posted to PG Each Day'), + cumulative_title: _('Cumulative Projects Posted to PG') + ); default: throw new UnexpectedValueException(html_safe("bad value for 'which': '$which'")); } - - return $obj; } From 975dc328769e37d5c81e8c7dfdd0ec9099bd6ac0 Mon Sep 17 00:00:00 2001 From: Brian Foley Date: Tue, 8 Sep 2026 22:32:10 +0100 Subject: [PATCH 4/7] TallyBoard.inc: get_vitals return Vitals DTO instead of stdClass --- pinc/TallyBoard.inc | 68 ++++++++++++++++++++++++++++----------------- 1 file changed, 42 insertions(+), 26 deletions(-) diff --git a/pinc/TallyBoard.inc b/pinc/TallyBoard.inc index fd2b1a21c..e0ee0beab 100644 --- a/pinc/TallyBoard.inc +++ b/pinc/TallyBoard.inc @@ -539,15 +539,11 @@ class TallyBoard ); $result = DPDatabase::query($sql); - $row = mysqli_fetch_assoc($result); - if (!$row) { + if (!is_array($row = mysqli_fetch_assoc($result))) { // No snapshots for this TallyBoard yet. - $max_timestamp = $default; - } else { - $max_timestamp = $row["max_timestamp"]; + return $default; } - - return $max_timestamp; + return (int) $row["max_timestamp"]; } // ------------------------------------------------------------------------- @@ -796,34 +792,54 @@ class TallyBoard * * This is just a convenience function. */ - public function get_vitals(int $holder_id): Object + public function get_vitals(int $holder_id): Vitals { - $obj = new StdClass(); - - $obj->current_tally = $this->get_current_tally($holder_id); - $obj->current_rank = $this->get_rank($holder_id); - + $current_tally = $this->get_current_tally($holder_id); + $current_rank = $this->get_rank($holder_id); $snapshot_info = $this->get_info_from_latest_snapshot($holder_id); - $obj->latest_snapshot_timestamp = $snapshot_info['timestamp']; - $obj->latest_snapshot_delta = $snapshot_info['tally_delta']; - $obj->latest_snapshot_tally = $snapshot_info['tally_value']; - [$obj->best_delta, $obj->best_delta_timestamp] = + [$best_delta, $best_delta_timestamp] = $this->get_info_re_largest_delta($holder_id); - $today_delta = $obj->current_tally - $obj->latest_snapshot_tally; - if ($today_delta > $obj->best_delta) { - $obj->best_delta = $today_delta; - $obj->best_delta_timestamp = time(); + $today_delta = $current_tally - $snapshot_info['tally_delta']; + if ($today_delta > $best_delta) { + $best_delta = $today_delta; + $best_delta_timestamp = time(); } - [$obj->best_rank, $obj->best_rank_timestamp] = + [$best_rank, $best_rank_timestamp] = $this->get_info_re_best_rank($holder_id); - if ($obj->current_rank < $obj->best_rank) { - $obj->best_rank = $obj->current_rank; - $obj->best_rank_timestamp = time(); + if ($current_rank < $best_rank) { + $best_rank = $current_rank; + $best_rank_timestamp = time(); } - return $obj; + return new Vitals( + current_tally: $current_tally, + current_rank: $current_rank, + latest_snapshot_timestamp: $snapshot_info['timestamp'], + latest_snapshot_delta: $snapshot_info['tally_delta'], + latest_snapshot_tally: $snapshot_info['tally_value'], + best_delta: $best_delta, + best_delta_timestamp: $best_delta_timestamp, + best_rank: $best_rank, + best_rank_timestamp: $best_rank_timestamp + ); + + } +} +class Vitals +{ + public function __construct( + public readonly int $current_tally, + public readonly int $current_rank, + public readonly int $latest_snapshot_timestamp, + public readonly int $latest_snapshot_delta, + public readonly int $latest_snapshot_tally, + public readonly int $best_delta, + public readonly int $best_delta_timestamp, + public readonly int $best_rank, + public readonly int $best_rank_timestamp, + ) { } } From aa31e501f49be7e27e1c50d73b573cf9de83882f Mon Sep 17 00:00:00 2001 From: Brian Foley Date: Tue, 8 Sep 2026 22:41:06 +0100 Subject: [PATCH 5/7] autorelease.inc: Add NumProjects DTO for maybe_release_project --- pinc/autorelease.inc | 59 +++++++++++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 22 deletions(-) diff --git a/pinc/autorelease.inc b/pinc/autorelease.inc index e637ce2ab..c02ff3b1d 100644 --- a/pinc/autorelease.inc +++ b/pinc/autorelease.inc @@ -207,16 +207,25 @@ function autorelease_for_round(Round $round): void // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +class NumProjects +{ + public function __construct( + public readonly int $waiting, + public readonly int $considered, + public readonly int $blocked, + public readonly int $failed, + public readonly int $released, + ) { + } +} function maybe_release_projects( Round $round, ?Object $qd, bool $observe_release_restrictions, bool $release_at_most_one -): Object { - $n_projects = new StdClass(); - +): NumProjects { $and_extra_condition = ($qd ? "AND ($qd->cooked_project_selector)" : ""); - $q_name = ($qd ? $qd->name : ""); + $q_name = $qd ? $qd->name : ""; // set of candidates for release: // Projects that are in this round's waiting state, @@ -240,24 +249,24 @@ function maybe_release_projects( "; $waiting_res = DPDatabase::query($sql); - $n_projects->waiting = mysqli_num_rows($waiting_res); + $waiting = mysqli_num_rows($waiting_res); echo "\n"; if ($qd) { - echo " There are {$n_projects->waiting} projects waiting for release in this queue.\n"; + echo " There are {$waiting} projects waiting for release in this queue.\n"; } else { - echo "There are {$n_projects->waiting} projects waiting for release in this round.\n"; + echo "There are {$waiting} projects waiting for release in this round.\n"; } - $n_projects->considered = 0; - $n_projects->blocked = 0; - $n_projects->failed = 0; - $n_projects->released = 0; + $considered = 0; + $blocked = 0; + $failed = 0; + $released = 0; while ($project = mysqli_fetch_assoc($waiting_res)) { echo "\n"; echo " considering {$project['projectid']} \"{$project['nameofwork']}\"...\n"; - $n_projects->considered++; + $considered++; if ($observe_release_restrictions) { $release_restrictor = ReleaseRestrictor::$restrictors[$round->id] ?? null; @@ -265,7 +274,7 @@ function maybe_release_projects( if (!$release_restrictor->approves_project($project)) { // Messages have been echoed. - $n_projects->blocked++; + $blocked++; // Go to the next project in the list. continue; } @@ -273,7 +282,7 @@ function maybe_release_projects( if (!attempt_to_release($round, $project, $q_name)) { // Something went wrong. Error messages have been output. - $n_projects->failed++; + $failed++; // Go to the next project in the list. continue; } @@ -282,7 +291,7 @@ function maybe_release_projects( echo " Project released!\n"; - $n_projects->released++; + $released++; $release_restrictor = ReleaseRestrictor::$restrictors[$round->id] ?? null; if ($release_restrictor) { @@ -324,17 +333,23 @@ function maybe_release_projects( } } - if ($n_projects->waiting > 0) { + if ($waiting > 0) { echo "\n"; - echo sprintf(" Of the %d projects that were waiting...\n", $n_projects->waiting); - echo sprintf(" %3d projects were blocked by release restrictions\n", $n_projects->blocked); - echo sprintf(" %3d projects failed the pre-release check or the state transition\n", $n_projects->failed); - echo sprintf(" %3d projects were released\n", $n_projects->released); - echo sprintf(" %3d projects were not considered\n", $n_projects->waiting - $n_projects->considered); + echo sprintf(" Of the %d projects that were waiting...\n", $waiting); + echo sprintf(" %3d projects were blocked by release restrictions\n", $blocked); + echo sprintf(" %3d projects failed the pre-release check or the state transition\n", $failed); + echo sprintf(" %3d projects were released\n", $released); + echo sprintf(" %3d projects were not considered\n", $waiting - $considered); echo "\n"; } - return $n_projects; + return new NumProjects( + $waiting, + $considered, + $blocked, + $failed, + $released + ); } // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX From e4df5dc9794fbbc47b5bd6cb9f3eb7e40139e9cc Mon Sep 17 00:00:00 2001 From: Brian Foley Date: Mon, 14 Sep 2026 23:38:06 +0100 Subject: [PATCH 6/7] autorelease.inc: Add QueueDefn DTO for maybe_release_projects --- pinc/autorelease.inc | 53 +++++++++++++++++++++++++++++--------------- 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/pinc/autorelease.inc b/pinc/autorelease.inc index c02ff3b1d..1b2548a2e 100644 --- a/pinc/autorelease.inc +++ b/pinc/autorelease.inc @@ -47,7 +47,33 @@ function attempt_to_release(Round $round, array $project, string $queue_name): b } - +class QueueDefn +{ + public readonly string $cooked_project_selector; + /** + * @param $name + * A human-readable name for this queue. + * + * $param $project_selector + * An SQL condition that defines the kind of project + * that can be released by this queue. + * + * @param $projects_target + * @param $pages_target + * Integers that specify the circumstances under which + * this queue can release a project. + * + * For more details, see SETUP/site_admin_notes.txt under "Defining queues". + */ + public function __construct( + public readonly string $name, + public readonly string $project_selector, + public readonly int $projects_target, + public readonly int $pages_target, + ) { + $this->cooked_project_selector = cook_project_selector($project_selector); + } +} function autorelease_for_round(Round $round): void { @@ -109,22 +135,13 @@ function autorelease_for_round(Round $round): void ); $q_res = DPDatabase::query($sql); - while ($qd = mysqli_fetch_object($q_res)) { - // $qd->name - // A human-readable name for this queue. - // - // $qd->project_selector - // An SQL condition that defines the kind of project - // that can be released by this queue. - // - // $qd->projects_target - // $qd->pages_target - // Integers that specify the circumstances under which - // this queue can release a project. - // - // For more details, see SETUP/site_admin_notes.txt under "Defining queues". - - $qd->cooked_project_selector = cook_project_selector($qd->project_selector); + while ($row = mysqli_fetch_assoc($q_res)) { + $qd = new QueueDefn( + (string) $row['name'], + (string) $row['project_selector'], + (int) $row['projects_target'], + (int) $row['pages_target'] + ); // Release available projects of a given type (defined by $qd->project_selector) // as long as the condition implied by the targets is true for projects of that type. @@ -220,7 +237,7 @@ class NumProjects } function maybe_release_projects( Round $round, - ?Object $qd, + ?QueueDefn $qd, bool $observe_release_restrictions, bool $release_at_most_one ): NumProjects { From c585a68892ad111b8afc41f641e5b5a31b71de56 Mon Sep 17 00:00:00 2001 From: Brian Foley Date: Tue, 8 Sep 2026 22:50:08 +0100 Subject: [PATCH 7/7] wordcheck_engine.inc: Add FileInfo DTO for get_file_info_object --- pinc/wordcheck_engine.inc | 50 +++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/pinc/wordcheck_engine.inc b/pinc/wordcheck_engine.inc index 21396051d..6e6455132 100644 --- a/pinc/wordcheck_engine.inc +++ b/pinc/wordcheck_engine.inc @@ -877,11 +877,9 @@ function get_wordcheck_file_names(): array * @param string $code * Must match one of the keys in array returned from `get_wordcheck_file_names()` * - * @return object - * * @see get_wordcheck_file_names() */ -function get_project_word_file(string $projectid, string $code): object +function get_project_word_file(string $projectid, string $code): FileInfoObject { global $projects_dir, $projects_url; @@ -910,7 +908,7 @@ function get_site_word_paths(): array * * @param 'good'|'bad'|'possible_bad' $code */ -function get_site_word_file(string $langcode3, string $code): object +function get_site_word_file(string $langcode3, string $code): FileInfoObject { [$site_words_dir, $site_words_url] = get_site_word_paths(); @@ -928,33 +926,35 @@ function get_site_word_file(string $langcode3, string $code): object ); } +class FileInfoObject +{ + public function __construct( + public readonly string $filename, + public readonly string $abs_path, + public readonly string $abs_url, + public readonly bool $exists, + public readonly int $size, + public readonly int $mod_time + ) { + } +} /** * Returns an object containing information about the specified * word-related file for the given project. - * - * Attributes include: - * - filename: its filename - * - abs_path: its absolute path - * - abs_url: its absolute URL - * - exists: a boolean indicating whether it exists - * - size: its size in bytes - * - mod_time: the time it was last modified, as a unix timestamp. - * Size and mod_time are set to zero if the file doesn't exist. */ -function get_file_info_object(string $filename, string $base_dir, string $base_url): Object +function get_file_info_object(string $filename, string $base_dir, string $base_url): FileInfoObject { clearstatcache(); - - $o = new StdClass(); - - $o->filename = $filename; - $o->abs_path = "$base_dir/{$o->filename}"; - $o->abs_url = "$base_url/{$o->filename}"; - $o->exists = @file_exists($o->abs_path); - $o->size = $o->exists ? filesize($o->abs_path) : 0; - $o->mod_time = $o->exists ? filemtime($o->abs_path) : 0; - - return $o; + $abs_path = "$base_dir/{$filename}"; + $exists = @file_exists($abs_path); + return new FileInfoObject( + filename: $filename, + abs_path: $abs_path, + abs_url: "$base_url/{$filename}", + exists: $exists, + size: $exists ? filesize($abs_path) : 0, + mod_time: $exists ? filemtime($abs_path) : 0, + ); } // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX