創新實訓(八)——題目相關的邏輯處理解釋

山城甘草發表於2024-06-23

題目部分

題目是整個OJ系統的練習基礎,無論是平時學生的練習還是比賽時的準備用題,題目系統在OJ中都是至關重要的。在controllers資料夾下,負責題目部分的程式碼檔案分別為:problem_set.php,problem.php,problem_statistics.php,problem_data_manage.php,problem_statement_manage.php,problem_managers_manage.php。以下是具體的詳細內容

題目列表

problem_set.php中,檔案主要用於管理和展示題目列表。以下是程式碼的詳細解釋:

1. 引入庫

requirePHPLib('form');
requirePHPLib('judger');
requirePHPLib('data');

這些函式用於引入需要的庫檔案,分別用於表單處理、評測系統和資料操作。

2. 新增題目按鈕

if (isSuperUser($myUser)) {
    $new_problem_form = new UOJForm('new_problem');
    $new_problem_form->handle = function() {
        DB::query("insert into problems (title, is_hidden, submission_requirement) values ('New Problem', 1, '{}')");
        $id = DB::insert_id();
        DB::query("insert into problems_contents (id, statement, statement_md) values ($id, '', '')");
        dataNewProblem($id);
    };
    $new_problem_form->submit_button_config['align'] = 'right';
    $new_problem_form->submit_button_config['class_str'] = 'btn btn-primary';
    $new_problem_form->submit_button_config['text'] = UOJLocale::get('problems::add new');
    $new_problem_form->submit_button_config['smart_confirm'] = '';
    
    $new_problem_form->runAtServer();
}
  • 檢查當前使用者是否是超級使用者,如果是,則顯示一個用於新增題目的表單。
  • 當表單提交時,插入一條新題目記錄到problems表,並獲取新題目的ID。
  • 插入一條空的題目內容記錄到problems_contents表。
  • 表單按鈕的樣式和文字配置。

3. 列印每個題目

function echoProblem($problem) {
    global $myUser;
    if (isProblemVisibleToUser($problem, $myUser)) {
        echo '<tr class="text-center">';
        if ($problem['submission_id']) {
            echo '<td class="success">';
        } else {
            echo '<td>';
        }
        echo '#', $problem['id'], '</td>';
        echo '<td class="text-left">';
        if ($problem['is_hidden']) {
            echo ' <span class="text-danger">[隱藏]</span> ';
        }
        echo '<a href="/problem/', $problem['id'], '">', $problem['title'], '</a>';
        if (isset($_COOKIE['show_tags_mode'])) {
            foreach (queryProblemTags($problem['id']) as $tag) {
                echo '<a class="uoj-problem-tag">', '<span class="badge badge-pill badge-secondary">', HTML::escape($tag), '</span>', '</a>';
            }
        }
        echo '</td>';
        if (isset($_COOKIE['show_submit_mode'])) {
            $perc = $problem['submit_num'] > 0 ? round(100 * $problem['ac_num'] / $problem['submit_num']) : 0;
            echo <<<EOD
            <td><a href="/submissions?problem_id={$problem['id']}&min_score=100&max_score=100">&times;{$problem['ac_num']}</a></td>
            <td><a href="/submissions?problem_id={$problem['id']}">&times;{$problem['submit_num']}</a></td>
            <td>
                <div class="progress bot-buffer-no">
                    <div class="progress-bar progress-bar-success" role="progressbar" aria-valuenow="$perc" aria-valuemin="0" aria-valuemax="100" style="width: $perc%; min-width: 20px;">{$perc}%</div>
                </div>
            </td>
EOD;
        }
        echo '<td class="text-left">', getClickZanBlock('P', $problem['id'], $problem['zan']), '</td>';
        echo '</tr>';
    }
}
  • echoProblem函式用於輸出每個題目的HTML。
  • 判斷題目是否對當前使用者可見,如果可見,則輸出題目的ID、標題、標籤、提交統計和點贊數。

4. 構建查詢條件

$cond = array();
$search_tag = null;
$cur_tab = isset($_GET['tab']) ? $_GET['tab'] : 'all';
if ($cur_tab == 'template') {
    $search_tag = "模板題";
}
if (isset($_GET['tag'])) {
    $search_tag = $_GET['tag'];
}
if ($search_tag) {
    $cond[] = "'".DB::escape($search_tag)."' in (select tag from problems_tags where problems_tags.problem_id = problems.id)";
}
if (isset($_GET["search"])) { 
    $cond[]="title like '%".DB::escape($_GET["search"])."%' or id like '%".DB::escape($_GET["search"])."%'";
}
if ($cond) {
    $cond = join($cond, ' and ');
} else {
    $cond = '1';
}
  • 根據URL引數構建查詢條件,用於篩選題目列表。

5. 定義表頭

$header = '<tr>';
$header .= '<th class="text-center" style="width:5em;">ID</th>';
$header .= '<th>'.UOJLocale::get('problems::problem').'</th>';
if (isset($_COOKIE['show_submit_mode'])) {
    $header .= '<th class="text-center" style="width:5em;">'.UOJLocale::get('problems::ac').'</th>';
    $header .= '<th class="text-center" style="width:5em;">'.UOJLocale::get('problems::submit').'</th>';
    $header .= '<th class="text-center" style="width:150px;">'.UOJLocale::get('problems::ac ratio').'</th>';
}
$header .= '<th class="text-center" style="width:180px;">'.UOJLocale::get('appraisal').'</th>';
$header .= '</tr>';
  • 定義題目列表的表頭,根據使用者的顯示設定調整顯示內容。

6. 定義分頁器和表格樣式

$pag_config = array('page_len' => 100);
$pag_config['col_names'] = array('*');
$pag_config['table_name'] = "problems left join best_ac_submissions on best_ac_submissions.submitter = '{$myUser['username']}' and problems.id = best_ac_submissions.problem_id";
$pag_config['cond'] = $cond;
$pag_config['tail'] = "order by id asc";
$pag = new Paginator($pag_config);

$div_classes = array('table-responsive');
$table_classes = array('table', 'table-bordered', 'table-hover', 'table-striped');
  • 設定分頁器的配置,包括每頁顯示的題目數量、查詢的表名和條件等。

7. 輸出HTML內容

<?php echoUOJPageHeader(UOJLocale::get('problems')) ?>
<div class="row">
    <div class="col-sm-4">
        <?= HTML::tablist($tabs_info, $cur_tab, 'nav-pills') ?>
    </div>
    <div class="col-sm-4 order-sm-9 checkbox text-right">
        <label class="checkbox-inline" for="input-show_tags_mode"><input type="checkbox" id="input-show_tags_mode" <?= isset($_COOKIE['show_tags_mode']) ? 'checked="checked" ': ''?>/> <?= UOJLocale::get('problems::show tags') ?></label>
        <label class="checkbox-inline" for="input-show_submit_mode"><input type="checkbox" id="input-show_submit_mode" <?= isset($_COOKIE['show_submit_mode']) ? 'checked="checked" ': ''?>/> <?= UOJLocale::get('problems::show statistics') ?></label>
    </div>
    <div class="col-sm-4 order-sm-5">
    <?php echo $pag->pagination(); ?>
    </div>
</div>
<div class="top-buffer-sm"></div>
<script type="text/javascript">
$('#input-show_tags_mode').click(function() {
    if (this.checked) {
        $.cookie('show_tags_mode', '', {path: '/problems'});
    } else {
        $.removeCookie('show_tags_mode', {path: '/problems'});
    }
    location.reload();
});
$('#input-show_submit_mode').click(function() {
    if (this.checked) {
        $.cookie('show_submit_mode', '', {path: '/problems'});
    } else {
        $.removeCookie('show_submit_mode', {path: '/problems'});
    }
    location.reload();
});
</script>
<?php
    echo '<div class="', join($div_classes, ' '), '">';
    echo '<table class="', join($table_classes, ' '), '">';
    echo '<thead>';
    echo $header;
    echo '</thead>';
    echo '<tbody>';
    
    foreach ($pag->get() as $idx => $row) {
        echoProblem($row);
        echo "\n";
    }
    if ($pag->isEmpty()) {
        echo '<tr><td class="text-center" colspan="233">'.UOJLocale::get('none').'</td></tr>';
    }
    
    echo '</tbody>';
    echo '</table>';
    echo '</div>';
    
    if (isSuperUser($myUser)) {
        $new_problem_form->printHTML();
    }
    
    echo $pag->

pagination();
?>
<?php echoUOJPageFooter() ?>
  • 輸出頁面頭部、標籤欄、分頁導航、題目列表表格和底部。
  • 使用JavaScript控制標籤和統計資訊的顯示。

題目頁面與提交

題目頁面與提交頁面下主要有”描述“、”提交“和”管理“(此功能只有管理員才能看到)功能。

當然,下面是對這段PHP程式碼的詳細解釋:

引入必要的PHP庫

requirePHPLib('form');
requirePHPLib('judger');
  • requirePHPLib('form'):引入處理表單的庫。
  • requirePHPLib('judger'):引入處理評測的庫。

驗證題目ID並查詢題目資訊

if (!validateUInt($_GET['id']) || !($problem = queryProblemBrief($_GET['id']))) {
    become404Page();
}
$problem_content = queryProblemContent($problem['id']);
  • 驗證 id 是否為正整數,且查詢題目的簡要資訊。
  • 如果題目不存在或ID無效,返回404頁面。
  • 查詢題目詳細內容。

處理競賽相關資訊

$contest = validateUInt($_GET['contest_id']) ? queryContest($_GET['contest_id']) : null;
if ($contest != null) {
    genMoreContestInfo($contest);
    $problem_rank = queryContestProblemRank($contest, $problem);
    if ($problem_rank == null) {
        become404Page();
    } else {
        $problem_letter = chr(ord('A') + $problem_rank - 1);
    }
}
  • 驗證 contest_id 是否為正整數,且查詢競賽資訊。
  • 如果競賽存在,生成更多競賽資訊並查詢題目在競賽中的排名。
  • 如果題目不在競賽中,返回404頁面。
  • 計算題目在競賽中的字母標識。

檢查使用者許可權和題目可見性

$is_in_contest = false;
$ban_in_contest = false;
if ($contest != null) {
    if (!hasContestPermission($myUser, $contest)) {
        if ($contest['cur_progress'] == CONTEST_NOT_STARTED) {
            become404Page();
        } elseif ($contest['cur_progress'] == CONTEST_IN_PROGRESS) {
            if ($myUser == null || !hasRegistered($myUser, $contest)) {
                becomeMsgPage("<h1>比賽正在進行中</h1><p>很遺憾,您尚未報名。比賽結束後再來看吧~</p>");
            } else {
                $is_in_contest = true;
                DB::update("update contests_registrants set has_participated = 1 where username = '{$myUser['username']}' and contest_id = {$contest['id']}");
            }
        } else {
            $ban_in_contest = !isProblemVisibleToUser($problem, $myUser);
        }
    }
} else {
    if (!isProblemVisibleToUser($problem, $myUser)) {
        become404Page();
    }
}
  • 初始化 is_in_contestban_in_contestfalse
  • 如果競賽存在且使用者無許可權:
    • 競賽未開始,返回404頁面。
    • 競賽進行中,且使用者未登入或未報名,顯示比賽進行中的提示資訊。
    • 如果使用者已報名,設定 is_in_contesttrue 並更新資料庫。
    • 競賽已結束,檢查題目是否對使用者可見,設定 ban_in_contest
  • 如果沒有競賽,檢查題目是否對使用者可見,不可見則返回404頁面。

獲取提交需求和額外配置

$submission_requirement = json_decode($problem['submission_requirement'], true);
$problem_extra_config = getProblemExtraConfig($problem);
$custom_test_requirement = getProblemCustomTestRequirement($problem);
  • 解析題目的提交需求和自定義測試需求。
  • 獲取題目的額外配置。

處理自定義測試狀態請求

if ($custom_test_requirement && $_GET['get'] == 'custom-test-status-details' && Auth::check()) {
    if ($custom_test_submission == null) {
        echo json_encode(null);
    } else if ($custom_test_submission['status'] != 'Judged') {
        echo json_encode(array(
            'judged' => false,
            'html' => getSubmissionStatusDetails($custom_test_submission)
        ));
    } else {
        ob_start();
        $styler = new CustomTestSubmissionDetailsStyler();
        if (!hasViewPermission($problem_extra_config['view_details_type'], $myUser, $problem, $submission)) {
            $styler->fade_all_details = true;
        }
        echoJudgementDetails($custom_test_submission_result['details'], $styler, 'custom_test_details');
        $result = ob_get_contents();
        ob_end_clean();
        echo json_encode(array(
            'judged' => true,
            'html' => getSubmissionStatusDetails($custom_test_submission),
            'result' => $result
        ));
    }
    die();
}
  • 如果有自定義測試需求,並且GET請求引數 getcustom-test-status-details 且使用者已登入:
    • 如果沒有自定義測試提交記錄,返回 null
    • 如果提交記錄的狀態不是 Judged,返回評測狀態詳情。
    • 否則,獲取評測詳情並返回評測結果。

檢查是否可以使用ZIP上傳

$can_use_zip_upload = true;
foreach ($submission_requirement as $req) {
    if ($req['type'] == 'source code') {
        $can_use_zip_upload = false;
    }
}
  • 檢查提交需求中是否包含原始碼,如果包含則不允許使用ZIP上傳。

處理ZIP檔案上傳

function handleUpload($zip_file_name, $content, $tot_size) {
    global $problem, $contest, $myUser, $is_in_contest;

    $content['config'][] = array('problem_id', $problem['id']);
    if ($is_in_contest && $contest['extra_config']["contest_type"] != 'IOI' && !isset($contest['extra_config']["problem_{$problem['id']}"])) {
        $content['final_test_config'] = $content['config'];
        $content['config'][] = array('test_sample_only', 'on');
    }
    $esc_content = DB::escape(json_encode($content));

    $language = '/';
    foreach ($content['config'] as $row) {
        if (strEndWith($row[0], '_language')) {
            $language = $row[1];
            break;
        }
    }
    if ($language != '/') {
        Cookie::set('uoj_preferred_language', $language, time() + 60 * 60 * 24 * 365, '/');
    }
    $esc_language = DB::escape($language);

    $result = array();
    $result['status'] = "Waiting";
    $result_json = json_encode($result);

    if ($is_in_contest) {
        DB::query("insert into submissions (problem_id, contest_id, submit_time, submitter, content, language, tot_size, status, result, is_hidden) values (${problem['id']}, ${contest['id']}, now(), '${myUser['username']}', '$esc_content', '$esc_language', $tot_size, '${result['status']}', '$result_json', 0)");
    } else {
        DB::query("insert into submissions (problem_id, submit_time, submitter, content, language, tot_size, status, result, is_hidden) values (${problem['id']}, now(), '${myUser['username']}', '$esc_content', '$esc_language', $tot_size, '${result['status']}', '$result_json', {$problem['is_hidden']})");
    }
}
  • 處理ZIP檔案上傳:
    • 根據上傳內容配置更新提交記錄。
    • 處理語言設定並儲存。
    • 將提交記錄插入資料庫。

處理自定義測試上傳

function handleCustomTestUpload($zip_file_name, $content, $tot_size) {
    global $problem, $contest, $myUser;

    $content['config'][] = array('problem_id', $problem['id']);
    $content['config'][] = array('custom_test', 'on');
    $esc_content = DB::escape(json_encode($content));

    $language = '/';
    foreach ($content['config'] as $row) {
        if (strEndWith($row[0], '_language')) {
            $language = $row[1];
            break;
        }
    }
    if ($language != '/') {
        Cookie::set('uoj_preferred_language', $language, time() + 60 * 60 * 24 * 365, '/');
    }
    $esc_language = DB::escape($language);

    $result = array();
    $result['status'] = "Waiting";
    $result_json = json_encode($result);

    DB::insert("insert into custom_test_submissions (problem_id, submit_time, submitter, content, status, result) values ({$problem['id']}, now(), '{$myUser['username']}', '$esc_content', '{$result['status']}', '$result_json')");
}
  • 類似ZIP檔案上傳的處理方式,處理自定義測試上傳並插入資料庫。

建立並處理表單

if ($can_use_zip_upload) {
    $zip_answer_form = newZipSubmissionForm('zip_answer',
        $submission_requirement,
        'uojRandAvaiableSubmissionFileName',
        'handleUpload');
   

 $zip_answer_form->runAtServer();
}

$answer_form = newSubmissionForm('answer',
    $submission_requirement,
    'handleUpload');
$answer_form->runAtServer();

if ($custom_test_requirement) {
    $custom_test_form = newSubmissionForm('custom_test',
        $custom_test_requirement,
        'handleCustomTestUpload');
    $custom_test_form->runAtServer();
}
  • 根據題目的提交需求,建立並處理不同型別的表單:
    • 如果允許ZIP上傳,建立ZIP提交表單。
    • 建立普通提交表單。
    • 如果有自定義測試需求,建立自定義測試表單。

管理員題目描述處理

這段程式碼實現了一個問題管理頁面,允許具有相應許可權的使用者編輯問題的內容、標籤和可見性

引入必要的庫

requirePHPLib('form');

這行程式碼引入了form庫,提供表單處理功能。

驗證和獲取問題資訊

if (!validateUInt($_GET['id']) || !($problem = queryProblemBrief($_GET['id']))) {
    become404Page();
}
  • validateUInt($_GET['id']):驗證GET引數id是否為無符號整數。
  • queryProblemBrief($_GET['id']):查詢問題簡要資訊。如果問題不存在或驗證失敗,呼叫become404Page(),返回404頁面。
if (!hasProblemPermission($myUser, $problem)) {
    become403Page();
}
  • hasProblemPermission($myUser, $problem):檢查當前使用者是否有許可權管理這個問題。如果沒有許可權,呼叫become403Page(),返回403頁面。

獲取問題詳細內容和標籤

$problem_content = queryProblemContent($problem['id']);
$problem_tags = queryProblemTags($problem['id']);
  • queryProblemContent($problem['id']):獲取問題的詳細內容。
  • queryProblemTags($problem['id']):獲取問題的標籤。

初始化編輯器

$problem_editor = new UOJBlogEditor();
$problem_editor->name = 'problem';
$problem_editor->blog_url = "/problem/{$problem['id']}";
$problem_editor->cur_data = array(
    'title' => $problem['title'],
    'content_md' => $problem_content['statement_md'],
    'content' => $problem_content['statement'],
    'tags' => $problem_tags,
    'is_hidden' => $problem['is_hidden']
);
$problem_editor->label_text = array_merge($problem_editor->label_text, array(
    'view blog' => '檢視題目',
    'blog visibility' => '題目可見性'
));
  • 建立一個新的UOJBlogEditor例項用於編輯問題內容。
  • 設定編輯器名稱、URL和當前資料(標題、內容、標籤和可見性)。
  • 更新編輯器的標籤文字(如“檢視題目”和“題目可見性”)。

定義儲存操作

$problem_editor->save = function($data) {
    global $problem, $problem_tags;
    DB::update("update problems set title = '".DB::escape($data['title'])."' where id = {$problem['id']}");
    DB::update("update problems_contents set statement = '".DB::escape($data['content'])."', statement_md = '".DB::escape($data['content_md'])."' where id = {$problem['id']}");
    
    if ($data['tags'] !== $problem_tags) {
        DB::delete("delete from problems_tags where problem_id = {$problem['id']}");
        foreach ($data['tags'] as $tag) {
            DB::insert("insert into problems_tags (problem_id, tag) values ({$problem['id']}, '".DB::escape($tag)."')");
        }
    }
    if ($data['is_hidden'] != $problem['is_hidden'] ) {
        DB::update("update problems set is_hidden = {$data['is_hidden']} where id = {$problem['id']}");
        DB::update("update submissions set is_hidden = {$data['is_hidden']} where problem_id = {$problem['id']}");
        DB::update("update hacks set is_hidden = {$data['is_hidden']} where problem_id = {$problem['id']}");
    }
};
  • 定義save函式以處理編輯器儲存操作。
  • 更新問題標題和內容(包括Markdown格式和HTML格式)。
  • 如果標籤發生變化,刪除舊標籤並插入新標籤。
  • 如果可見性發生變化,更新問題、提交的可見性。

題目資料管理

problem_data_manage.php中,包含著對於一道題目資料上傳的邏輯操作。這裡擷取部分重要程式碼進行解釋

資料上傳程式碼

<div class="modal fade" id="UploadDataModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
  		<div class="modal-dialog">
    			<div class="modal-content">
      				<div class="modal-header">
						<h4 class="modal-title" id="myModalLabel">上傳資料</h4>
        				<button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">&times;</span><span class="sr-only">Close</span></button>
      				</div>
      				<div class="modal-body">
        				<form action="" method="post" enctype="multipart/form-data" role="form">
							<div class="form-group">
									<label for="exampleInputFile">上傳zip檔案</label>
									<input type="file" name="problem_data_file" id="problem_data_file">
									<p class="help-block">說明:請將所有資料放置於壓縮包根目錄內。若壓縮包內僅存在資料夾而不存在檔案,則會將這些一級子資料夾下的內容移動到根目錄下,然後這些一級子資料夾刪除;若這些子資料夾記憶體在同名檔案,則會發生隨機替換,僅保留一個副本。</p>
							</div>
							<input type="hidden" name="problem_data_file_submit" value="submit">
      				</div>
      				<div class="modal-footer">
						<button type="submit" class="btn btn-success">上傳</button>
						</form>
        				<button type="button" class="btn btn-secondary" data-dismiss="modal">關閉</button>
      				</div>
    			</div>
  		</div>
	</div>
  • 使用者可以點選上傳資料按鈕,彈出此模態框。
  • 使用者可以選擇一個ZIP檔案,並點選模態框底部的上傳按鈕來提交選擇的檔案。
  • 提交表單時,資料將被提交到當前頁面(action=""表示當前頁面),並使用POST方法傳輸資料。
  • 上傳的ZIP檔案將會作為名為 problem_data_file 的檔案上傳欄位提交。
  • 提交完成後,可以點選模態框右上角的關閉按鈕或底部的關閉按鈕來關閉模態框。

線上顯示測試資料

這段程式碼定義了一個名為 getDataDisplayer() 的函式,它根據問題的配置資訊動態生成並返回一個 DataDisplayer 物件,用於顯示不同型別的資料檔案和資訊。讓我們逐步解釋其功能和實現細節:

1. 全域性變數宣告:

global $data_dir;
global $problem;
  • 這些變數包含了資料目錄路徑 $data_dir 和與問題相關的資訊 $problem

2. 獲取允許顯示的檔案列表:

$allow_files = array_flip(array_filter(scandir($data_dir), function($x){return $x !== '.' && $x !== '..';}));
  • 使用 scandir($data_dir) 獲取 $data_dir 目錄下的所有檔案和目錄。
  • 使用 array_filter 過濾掉當前目錄 ('.') 和上級目錄 ('..')。
  • 使用 array_flip 將檔名作為鍵,便於後續快速查詢檔案是否存在。

3. 定義獲取檔案顯示函式的匿名函式:

$getDisplaySrcFunc = function($name) use($allow_files) {
    return function() use($name, $allow_files) {
        $src_name = $name . '.cpp';
        if (isset($allow_files[$src_name])) {
            echoFilePre($src_name);
        } else {
            echoFileNotFound($src_name);
        }
        if (isset($allow_files[$name])) {
            echoFilePre($name);
        } else {
            echoFileNotFound($name);
        }
    };
};
  • $getDisplaySrcFunc 是一個匿名函式,接受一個檔名 $name,返回一個函式。
  • 返回的函式根據 $name$name . '.cpp'$allow_files 中查詢檔案是否存在,存在則呼叫 echoFilePre($file_name) 顯示檔案預覽,否則呼叫 echoFileNotFound($file_name) 顯示檔案未找到的資訊。

4. 讀取問題的配置檔案 problem.conf:

$problem_conf = getUOJConf("$data_dir/problem.conf");
if ($problem_conf === -1) {
    // 處理無法讀取問題配置檔案的情況
}
if ($problem_conf === -2) {
    // 處理問題配置檔案格式錯誤的情況
}
  • 使用 getUOJConf("$data_dir/problem.conf") 函式讀取問題的配置檔案內容。
  • 根據返回值 -1-2 處理無法讀取或格式錯誤的情況,分別顯示相應的錯誤資訊。

5. 根據配置檔案中的資訊設定顯示器和處理器:

  • 根據 use_builtin_judger 的設定和其他配置資訊動態生成 DataDisplayer 物件,並設定相應的資料顯示和處理方法。
// 處理使用內建判題器的情況
if ($problem_conf['use_builtin_judger'] == 'on') {
    // 設定顯示器用於顯示測試資料和額外測試資料
    // 設定顯示器用於顯示checker等其他型別資料
    // 根據hackable屬性設定顯示器用於顯示standard和validator
    // 根據interaction_mode設定顯示器用於顯示interactor
}

題目資料統計

在單個題目的主頁面的右上角中,有一個“統計”按鈕。點選按鈕即可顯示當前題目的相關資料資訊。下面列舉一些重要邏輯程式碼:

題目資料相關資訊的處理

	// 分數分佈資料的獲取
	function scoreDistributionData() {
		$data = array();
		$result = DB::select("select score, count(*) from submissions where problem_id = {$_GET['id']} and score is not null group by score");
		$is_res_empty = true;
		$has_score_0 = false;
		$has_score_100 = false;
		while ($row = DB::fetch($result, MYSQLI_NUM)) {
			if ($row[0] == 0) {
				$has_score_0 = true;
			} else if ($row[0] == 100) {
				$has_score_100 = true;
			}
			$score = $row[0] * 100;
			$data[] = array('score' => $score, 'count' => $row[1]);
		}
		if (!$has_score_0) {
			array_unshift($data, array('score' => 0, 'count' => 0));
		}
		if (!$has_score_100) {
			$data[] = array('score' => 10000, 'count' => 0);
		}
		return $data;
	}
	
	// 分數分佈資料的預處理
	$data = scoreDistributionData();
	$pre_data = $data;
	$suf_data = $data;
	for ($i = 0; $i < count($data); $i++) {
		$data[$i]['score'] /= 100;
	}
	for ($i = 1; $i < count($data); $i++) {
		$pre_data[$i]['count'] += $pre_data[$i - 1]['count'];
	}
	for ($i = count($data) - 1; $i > 0; $i--) {
		$suf_data[$i - 1]['count'] += $suf_data[$i]['count'];
	}
	
	// 提交排序選擇的處理
	$submissions_sort_by_choice = !isset($_COOKIE['submissions-sort-by-code-length']) ? 'time' : 'tot_size';
  • 從資料庫中查詢特定問題 (problem_id = $_GET['id']) 的提交資料,並統計每個分數的提交次數。
  • 如果沒有分數為0或100的記錄,手動新增這些分數和對應的次數。
  • 返回一個陣列 $data,包含分數和對應的提交次數。
  • 使用 scoreDistributionData() 函式獲取分數分佈資料並儲存在 $data 陣列中。
  • 複製 $data$pre_data$suf_data,用於預處理。
  • $data 陣列中的每個分數除以100,以得到真實的分數值。
  • $pre_data 陣列中每個元素的 count 屬性表示該分數及之前所有分數的累計提交次數。
  • $suf_data 陣列中每個元素的 count 屬性表示該分數及之後所有分數的累計提交次數。
  • 檢查是否設定了名為 submissions-sort-by-code-length 的 Cookie。
  • 如果未設定該 Cookie,將 $submissions_sort_by_choice 設定為 'time',表示按時間排序。
  • 如果設定了該 Cookie,將 $submissions_sort_by_choice 設定為 'tot_size',表示按程式碼長度排序。

使用Morris資料視覺化

程式碼使用了 Morris.js 庫來生成一個折線圖,並對資料進行了一些配置和處理。

new Morris.Line({
    element: 'score-distribution-chart-suf', // 圖表要渲染的元素的 ID
    data: <?= json_encode($suf_data) ?>, // 圖表使用的資料,使用 PHP 的 json_encode 將 PHP 陣列轉換為 JavaScript 物件
    xkey: 'score', // X 軸上的資料欄位
    ykeys: ['count'], // Y 軸上的資料欄位
    labels: ['number'], // 資料欄位的標籤,用於圖例和提示資訊
    lineColors: function(row, sidx, type) { // 定義折線的顏色
        if (type == 'line') {
            return '#0b62a4'; // 折線顏色
        }
        return getColOfScore(row.src.score / 100); // 如果有其他型別的資料,使用自定義函式獲取顏色
    },
    xLabelFormat: function(x) { // 定義 X 軸標籤的格式化函式
        return (x.getTime() / 100).toString(); // 將時間轉換為指定格式的字串
    },
    hoverCallback: function(index, options, content, row) { // 定義滑鼠懸停提示資訊的回撥函式
        var scr = row.score / 100; // 獲取分數,並轉換為實際的分數值
        return '<div class="morris-hover-row-label">' + 'score: &ge;' + scr + '</div>' + // 返回自定義的懸停提示資訊的 HTML
            '<div class="morris-hover-point">' + '<a href="/submissions?problem_id=' + <?= $problem['id'] ?> + '&amp;min_score=' + scr + '">' + 'number: ' + row.count + '</a>' + '</div>';
    },
    resize: true // 是否支援自動調整大小
});

相關文章