Overview
The Scoreboard model generates and caches HTML scoreboards for assignments. It calculates rankings, scores, penalties, and problem statistics based on final submissions.
Fillable Fields
ID of the assignment this scoreboard belongs toUnique constraint: One scoreboard per assignment
Cached HTML content of the rendered scoreboard tableMinified to reduce storage size
Relationships
assignment()
The assignment this scoreboard displays results for
$scoreboard->assignment // Returns Assignment model
Public Methods
update_scoreboard()
Static method to generate or update a scoreboard with rate limiting.
public static function update_scoreboard($assignment_id)
Parameters:
$assignment_id (integer) - Assignment ID to update
Returns: boolean - True if updated, false if skipped
Rate limiting: Only updates once every 30 seconds per assignment
Example:
// Called after new submission
Scoreboard::update_scoreboard($assignmentId);
// Skips if updated less than 30 seconds ago
Scoreboard::update_scoreboard($assignmentId);
Behavior:
- Practice assignment (id=0): Returns
false, no scoreboard created
- First time: Creates new scoreboard record
- Subsequent: Updates existing if > 30 seconds since last update
Private Methods
_update_scoreboard()
Generates scoreboard data and saves HTML.
private function _update_scoreboard()
Returns: boolean
Process:
- Calls
_generate_scoreboard() to calculate data
- Passes data to
scoreboard_table view
- Minifies HTML output
- Saves to
scoreboard field
_generate_scoreboard()
Calculates all scoreboard data from submissions.
private function _generate_scoreboard()
Returns: array containing:
$scores - Detailed scores per user per problem
$scoreboard - Sorted arrays for table display
$number_of_submissions - Submission counts
$stat_print - Problem statistics
Scoreboard Data Structure
User Scores Array
$scores = [
'john_doe' => [
'id' => 123,
10 => [ // problem_id
'score' => 80,
'time' => CarbonInterval, // Time from start
'late' => CarbonInterval, // Time after deadline (negative if early)
'fullmark' => false,
],
11 => [
'score' => 100,
'time' => CarbonInterval,
'late' => CarbonInterval,
'fullmark' => true,
],
],
'jane_doe' => [...],
];
Scoreboard Rankings Array
$scoreboard = [
'username' => ['john_doe', 'jane_doe', ...],
'score' => [180, 150, ...], // Total score
'accepted_score' => [100, 100, ...], // Score from fullmark problems only
'submit_penalty' => [CarbonInterval, ...], // Penalty time
'solved' => [1, 2, ...], // Number of fullmark problems
'tried_to_solve' => [2, 3, ...], // Number of attempted problems
'lops' => [ // Class names
'john_doe' => 'CS101',
'jane_doe' => 'CS102',
],
];
Problem Statistics
$stat_print = [
10 => [ // problem_id
'solved_tries' => '15 / 45 (33.33%)',
'solved_tries_users' => '10 / 20 (50%) (66.67%)',
'average_tries' => '2.3',
'average_tries_2_solve' => '3.0',
],
];
Scoring Algorithm
1. Calculate Pre-Score
$pre_score = ceil(
$submission->pre_score *
$problem->pivot->score / 10000
);
2. Apply Late Coefficient
if ($submission->coefficient === 'error') {
$final_score = 0;
} else {
$final_score = ceil($pre_score * $submission->coefficient / 100);
}
3. Determine Fullmark
$fullmark = ($submission->pre_score == 10000);
4. Calculate Time and Lateness
// Time from assignment start to submission
$time = CarbonInterval::seconds(
$assignment->start_time->diffInSeconds($submission->created_at, true)
)->cascade();
// Time from deadline to submission (negative = early)
$late = CarbonInterval::seconds(
$assignment->finish_time->diffInSeconds($submission->created_at)
)->cascade();
5. Calculate Penalty
Penalty only applies to fullmark submissions with score > 0:
if ($fullmark && $final_score > 0) {
$penalty->add(
$time->totalSeconds +
($number_of_submissions - 1) * Setting::get('submit_penalty'),
'seconds'
);
}
Penalty formula:
penalty = submission_time + (failed_attempts * submit_penalty)
Ranking Sort Order
Scoreboards are sorted by:
- Accepted Score (descending) - Score from fullmark problems only
- Penalty Time (ascending) - Lower penalty ranks higher
- Solved Count (descending) - More fullmark problems ranks higher
- Total Score (descending) - Total points including partial credit
- Username (alphabetically)
array_multisort(
$scoreboard['accepted_score'], SORT_NUMERIC, SORT_DESC,
$scoreboard['submit_penalty'], SORT_NATURAL, SORT_ASC,
$scoreboard['solved'], SORT_NUMERIC, SORT_DESC,
$scoreboard['score'], SORT_NUMERIC, SORT_DESC,
$scoreboard['username']
);
Problem Statistics Calculation
Aggregation Queries
// Count all submissions per user-problem
$aggr = $assignment->submissions()
->groupBy('user_id', 'problem_id')
->select(DB::raw('user_id, problem_id, count(*) as submit'))
->get();
// Count accepted submissions per user-problem
$aggr_ac = $assignment->submissions()
->groupBy('user_id', 'problem_id')
->where('pre_score', 10000)
->select(DB::raw('user_id, problem_id, count(*) as submit'))
->get();
Statistics Structure
$statistics[$problem_id] = [
'tries' => 45, // Total submissions
'tries_user' => 20, // Users who attempted
'solved' => 15, // Accepted submissions
'solved_user' => 10, // Users who solved
];
Display Calculations
// "15 / 45 (33.33%)"
$solved_tries = "$solved / $tries (" . round($solved * 100 / $tries, 2) . "%)";
// "10 / 20 (50%) (66.67%)"
$solved_tries_users = "$solved_user / $tries_user ("
. round($solved_user * 100 / $tries_user, 2) . "%) ("
. round($solved_user * 100 / count($users), 2) . "%)";
// Average attempts per user
$average_tries = round($tries / $tries_user, 1);
// Average attempts to solve
$average_tries_2_solve = round($tries / $solved, 1);
HTML Minification
The generated HTML is minified to reduce database storage:
// Remove newlines, returns, tabs
$scoreboard_table = str_replace(["\n", "\r", "\t"], '', $scoreboard_table);
// Collapse multiple spaces to single space
$scoreboard_table = preg_replace('/ {2,}/', ' ', $scoreboard_table);
Example Usage
// Update scoreboard after new submission
Scoreboard::update_scoreboard($assignmentId);
// Get scoreboard for display
$scoreboard = Scoreboard::where('assignment_id', $assignmentId)->first();
echo $scoreboard->scoreboard; // Outputs HTML table
// Get underlying assignment
$assignment = $scoreboard->assignment;
echo "Scoreboard for: {$assignment->name}";
// Force update (bypasses rate limit)
$scoreboard = Scoreboard::firstOrCreate(
['assignment_id' => $assignmentId],
['scoreboard' => '']
);
$scoreboard->_update_scoreboard();
Scoreboard Display Modes
While not enforced by the model, scoreboards can be displayed in different modes:
Full Scoreboard
Shows all participants with complete scores and statistics
Frozen Scoreboard
Stops updating at a certain time (typically 1 hour before contest end) - implemented at application level
User-Specific View
Students see only their own row - filtered at controller level
View Integration
The scoreboard uses the scoreboard_table Blade view:
$data = [
'assignment_id' => $assignment->id,
'problems' => $all_problems, // Problem models
'total_score' => $total_score, // Sum of all problem scores
'scores' => $scores, // User-problem scores
'scoreboard' => $scoreboard, // Sorted rankings
'names' => $result, // Username => display_name
'stat_print' => $stat_print, // Problem statistics
'no_of_problems' => $assignment->problems->count(),
'number_of_submissions' => $number_of_submissions,
];
$html = view('scoreboard_table', $data)->render();
- Rate Limiting: 30-second minimum between updates prevents excessive recalculation
- Caching: HTML stored in database, served directly without recalculation
- Eager Loading: Uses
with() to optimize queries:
$assignment->lops()->with('users')->get()
$assignment->submissions->where('is_final', 1)
- Indexed Queries: Queries use indexed columns (user_id, problem_id, is_final)
Special Cases
Practice Assignment (id=0)
No scoreboard is generated for the practice assignment:
if ($assignment_id != 0) {
// Generate scoreboard
}
No Submissions
If assignment has no final submissions, generates empty scoreboard:
$scoreboard = [
'username' => [],
'score' => [],
'accepted_score' => [],
'submit_penalty' => [],
'solved' => [],
'tried_to_solve' => [],
'lops' => [],
];
Multiple Classes
Users can belong to multiple lops, but scoreboard shows first match:
foreach ($assignment->lops()->with('users')->get() as $lop) {
foreach ($lop->users as $user) {
$lopsnames[$user->username] = $lop->name; // Last lop wins
}
}