From 5ebcedce4b850ac0764b26eaa96a03eb435ca464 Mon Sep 17 00:00:00 2001 From: TUNG-YEN Date: Fri, 6 Nov 2026 08:17:58 +0800 Subject: [PATCH 1/8] Add regression coverage for result field rendering --- tests/functional/ResultFieldRenderingCest.php | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 tests/functional/ResultFieldRenderingCest.php diff --git a/tests/functional/ResultFieldRenderingCest.php b/tests/functional/ResultFieldRenderingCest.php new file mode 100644 index 0000000..bf32371 --- /dev/null +++ b/tests/functional/ResultFieldRenderingCest.php @@ -0,0 +1,151 @@ +haveFixtures($this->_fixtures()); + $I->amLoggedInAsAdmin('sa'); + } + + protected function pages(): array + { + $cases = []; + foreach (['zh-TW', 'en-US'] as $language) { + foreach (['ballot-work/result', 'result/index', 'vote/result', + 'vote/vote-detail', 'ballot/edit', 'selected'] as $page) { + $cases[] = ['page' => $page, 'language' => $language, 'encoded' => false]; + } + // Existing encoded data must stay literal, rather than being decoded into markup. + $cases[] = ['page' => 'vote/result', 'language' => $language, 'encoded' => true]; + } + return $cases; + } + + /** @dataProvider pages */ + public function testRendering(FunctionalTester $I, Example $case): void + { + $voteID = 'AnonPartyTest'; + $transaction = Yii::$app->db->beginTransaction(); + $originalLanguage = Yii::$app->language; + $sessionLanguage = Yii::$app->session->get('language'); + try { + $config = CandiConfig::findOne(['voteID' => $voteID]); + $candidate = CandiData::find()->where(['voteID' => $voteID])->orderBy('id')->one(); + $resultConfig = ResultsConfig::findOne(['voteID' => $voteID]); + Assert::assertNotNull($config); + Assert::assertNotNull($candidate); + Assert::assertNotNull($resultConfig); + $fields = array_merge(['Name', 'instName', 'title'], array_map(fn ($c) => 'otherCol'.$c, range('A', 'F'))); + $labels = ['Name' => 'R&D', 'NameE' => 'R&D']; + $values = []; + foreach (range('A', 'F') as $letter) { + foreach (['' => 'Zh', 'E' => 'En'] as $suffix => $locale) { + $labels['otherColName'.$letter.$suffix] = 'L'.$letter.$locale.'
R&D'; + $values['otherCol'.$letter.$suffix] = 'D'.$letter.$locale.'
R&D'; + } + } + foreach (['instName', 'title'] as $field) { + foreach (['', 'E'] as $suffix) { + $values[$field.$suffix] = 'R&D
'; + } + } + if ($case['encoded']) { + $values['otherColA'] = $values['otherColAE'] = 'Literal<br>R&D'; + } + $config->updateAttributes(array_merge($labels, [ + 'showFieldSort' => json_encode(array_map(fn ($id) => ['id' => $id, 'children' => []], $fields)), + 'useBeforeHeader' => 0, + ])); + $candidate->updateAttributes($values); + $resultConfig->updateAttributes(['showFieldSort' => implode(',', $fields).',ballotCounts,elected']); + $results = new FormResults(); + $results->deleteResults($voteID, 1); + Assert::assertTrue($results->createResults($voteID)); + $before = FormResults::find()->where(['voteID' => $voteID])->orderBy('candID')->asArray()->all(); + Yii::$app->language = $case['language']; + Yii::$app->session->set('language', $case['language']); + if ($case['page'] === 'selected') { + $I->amOnPage('/vote/result?voteID='.$voteID.'&hl='.$case['language']); + $html = Yii::$app->view->renderFile(Yii::getAlias('@app/views/vote/_ballot-selected.php'), [ + 'model' => new FormManageVote($voteID, '0'), + 'gridID' => 'selected-rendering', + 'ballotCandi' => new ActiveDataProvider(['query' => FormResults::find()->where(['voteID' => $voteID])]), + 'candiConfig' => (new \app\models\FormCandiConfig())->getConfigWithVoteID($voteID), + 'questions' => [], + ], Yii::$app->controller); + } else { + $extra = $case['page'] === 'ballot/edit' ? '&party=0&ballotID=1' : ''; + $I->amOnPage('/'.$case['page'].'?voteID='.$voteID.$extra.'&hl='.$case['language']); + $I->seeResponseCodeIs(200); + $html = $I->grabPageSource(); + } + $dom = new DOMDocument(); + $previous = libxml_use_internal_errors(true); + try { + $dom->loadHTML(''.$html); + } finally { + libxml_clear_errors(); + libxml_use_internal_errors($previous); + } + $xpath = new DOMXPath($dom); + Assert::assertSame(0, $xpath->query('//script[contains(., "p16attack()")] | //*[@onclick="p16attack()" or @onerror="p16attack()"]')->length); + $locale = $case['language'] === 'zh-TW' ? 'Zh' : 'En'; + foreach (range('A', 'F') as $letter) { + $headers = $xpath->query('//th[contains(., "L'.$letter.$locale.'")]'); + Assert::assertGreaterThan(0, $headers->length, 'Custom header is present'); + Assert::assertGreaterThan(0, $xpath->query('.//br', $headers->item(0))->length, 'Header has a real break'); + Assert::assertSame('R&D', $xpath->query('.//b', $headers->item(0))->item(0)->textContent); + if ($letter === 'A' && $case['encoded']) { + $cells = $xpath->query('//td[contains(., "Literal")]'); + Assert::assertGreaterThan(0, $cells->length); + Assert::assertSame('Literal
R&D', trim($cells->item(0)->textContent)); + Assert::assertSame(0, $xpath->query('.//br', $cells->item(0))->length, 'Stored entities remain literal'); + continue; + } + $cells = $xpath->query('//td[contains(., "D'.$letter.$locale.'")]'); + Assert::assertGreaterThan(0, $cells->length, 'Custom value is present'); + $cell = $cells->item(0); + if ($letter === 'A') { + Assert::assertGreaterThan(0, $xpath->query('.//br', $cell)->length, 'Field A has a real break'); + Assert::assertSame('R&D', $xpath->query('.//b', $cell)->item(0)->textContent); + } else { + Assert::assertSame(0, $xpath->query('.//br | .//b', $cell)->length, 'B-F stay plain text'); + Assert::assertSame($values['otherCol'.$letter.($locale === 'Zh' ? '' : 'E')], trim($cell->textContent)); + } + } + Assert::assertGreaterThan(0, $xpath->query('//td[normalize-space(.)="R&D
"]')->length, 'Plain text is encoded once'); + Assert::assertSame($before, FormResults::find()->where(['voteID' => $voteID])->orderBy('candID')->asArray()->all(), 'Rendering leaves counts, ranks and elected status unchanged'); + } finally { + Yii::$app->language = $originalLanguage; + if ($sessionLanguage === null) { + Yii::$app->session->remove('language'); + } else { + Yii::$app->session->set('language', $sessionLanguage); + } + if ($transaction->isActive) { + $transaction->rollBack(); + } + } + } +} From a0c8f5c7f69d5d36dc8c97bf0d5f283656282d94 Mon Sep 17 00:00:00 2001 From: TUNG-YEN Date: Fri, 6 Nov 2026 08:22:26 +0800 Subject: [PATCH 2/8] Render result fields once with safe custom markup --- CHANGELOG.md | 1 + tests/e2e/README.md | 1 + tests/e2e/run.php | 2 +- tests/e2e/suites/result-fields.php | 107 ++++++++++++++++++ tests/functional/ResultFieldRenderingCest.php | 13 +++ views/ballot-work/result.php | 45 ++++---- views/ballot/ballot.php | 40 ++++--- views/result/index.php | 45 ++++---- views/vote/_ballot-selected.php | 47 ++++---- views/vote/_candi-list.php | 33 +++--- views/vote/result.php | 45 ++++---- 11 files changed, 271 insertions(+), 108 deletions(-) create mode 100644 tests/e2e/suites/result-fields.php diff --git a/CHANGELOG.md b/CHANGELOG.md index e625922..6659d09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ ### Fixed +* 開票結果、投票結果、候選名單與選票編輯表格消除重複 HTML 編碼;自訂標題及欄 A 保留經淨化的換行與排版,欄 B–F 維持純文字,既有 entity 不作資料轉換 * 英文介面的紙本選票自訂文字欄位誤讀多一個 E 的欄名而發生錯誤;保留中英並列與原有數字欄位的語言選擇 * Yii log 檢視頁的 UrlModal 資產指向未隨 Git 提供的目錄而發生 500;改為追蹤前端腳本,內容載入沿用目前頁面的協定與主機 * 殘留的 Bootstrap 4 樣式在 Bootstrap 5 下失效:輪次與投票狀態徽章看不見、對話框關閉鈕錯位,核取方塊、輸入群組、間距與邊框也一併改為 Bootstrap 5 寫法 diff --git a/tests/e2e/README.md b/tests/e2e/README.md index 77fbe68..7b9f27c 100644 --- a/tests/e2e/README.md +++ b/tests/e2e/README.md @@ -40,6 +40,7 @@ php tests/e2e/run.php --base=http://localhost:18080 | 組別 | 內容 | |---|---| +| `result-fields` | 中英文結果頁、名單與選票編輯的安全換行及純文字;攔截 GridView 產生的 CSV/Excel/HTML 匯出內容,檢查編碼與淨化,並檢查紙本選票。測試資料與欄位設定於 finally 還原;不包含 LibreOffice 開檔驗證 | | `pages` | 全畫面巡檢(投票者 6 頁、管理介面 56 頁),每頁檢查:HTTP 狀態、未被導向登入、無 PHP 錯誤訊息、無未捕捉的 JS 例外、無 Bootstrap 4 殘留 class、徽章看得見(非白字透明底)、無破圖,並截圖 | | `header` | 頁首輪次/投票狀態徽章、捲動後的投票名稱徽章 | | `modals` | 對話框 × 位置、Esc/點背景不關閉(static)、× 可關閉;重新載入 8 次確認遮罩不會蓋住對話框;新增群組成員對話框 | diff --git a/tests/e2e/run.php b/tests/e2e/run.php index 821992e..8e164b8 100644 --- a/tests/e2e/run.php +++ b/tests/e2e/run.php @@ -18,7 +18,7 @@ $opt = getopt('', ['base:', 'cdp:', 'chrome:', 'suite:']); $base = $opt['base'] ?? getenv('E2E_BASE_URL') ?: 'http://localhost:8080'; $cdpUrl = rtrim($opt['cdp'] ?? getenv('E2E_CDP_URL') ?: 'http://127.0.0.1:9222', '/'); -$allSuites = ['pages', 'header', 'modals', 'attachments', 'photos', 'results', 'voting', 'anon', 'layout']; +$allSuites = ['pages', 'header', 'modals', 'attachments', 'photos', 'results', 'result-fields', 'voting', 'anon', 'layout']; $suites = isset($opt['suite']) ? array_intersect(explode(',', $opt['suite']), $allSuites) : $allSuites; $outDir = dirname(__DIR__).'/_output/e2e'; @mkdir($outDir.'/files', 0777, true); diff --git a/tests/e2e/suites/result-fields.php b/tests/e2e/suites/result-fields.php new file mode 100644 index 0000000..d27aa34 --- /dev/null +++ b/tests/e2e/suites/result-fields.php @@ -0,0 +1,107 @@ +section('result-fields'); +$vote = 'AnonPartyTest'; +$configUrl = "/candi/config?voteID=$vote&questionID=1&hl=zh-TW"; +$manualUrl = "/candi/manual?voteID=$vote&action=update&id=1&hl=zh-TW"; +$resultUrl = "/count/result?voteID=$vote&hl=zh-TW"; +$readFields = function ($url, $ids) use ($admin) { + $admin->go($url); + return $admin->js('(() => { const out = {}; for (const id of '.json_encode($ids).') { + const e = document.getElementById(id); if (!e) throw new Error("Missing field: " + id); + out[id] = e.value; + } return out; })()'); +}; +$saveFields = function ($url, $values) use ($admin) { + $admin->go($url); + $admin->js('(() => { for (const [id, value] of Object.entries('.json_encode($values).')) { + const e = document.getElementById(id); if (!e) throw new Error("Missing field: " + id); + e.value = value; + } })()'); + $admin->click("[...document.querySelectorAll('button[type=submit]')].find(e => e.innerText.trim() === '修改')"); + $admin->waitLoad(); + if (!$admin->waitFor("![...document.querySelectorAll('.invalid-feedback,.help-block')].some(e => e.offsetParent !== null && e.innerText.trim())", 1000)) { + throw new RuntimeException('Form validation failed: '.implode(' | ', $admin->flashes())); + } +}; +$originalConfig = $readFields($configUrl, ['formcandiconfig-showfieldsort', 'formcandiconfig-othercolnamea', 'formcandiconfig-othercolnameae']); +$originalResults = $readFields($resultUrl, ['formresultsconfig-showfieldsort']); +$candidateOriginal = null; +try { + $fields = json_decode($originalConfig['formcandiconfig-showfieldsort'], true); + foreach (['otherColA', 'otherColB'] as $field) { + if (!in_array($field, array_column($fields, 'id'))) { + $fields[] = ['id' => $field, 'children' => []]; + } + } + $saveFields($configUrl, [ + 'formcandiconfig-showfieldsort' => json_encode($fields), + 'formcandiconfig-othercolnamea' => 'P16LabelZh
R&D', + 'formcandiconfig-othercolnameae' => 'P16LabelEn
R&D', + ]); + $candidateOriginal = $readFields($manualUrl, ['formcandidata-othercola', 'formcandidata-othercolae', 'formcandidata-othercolb', 'formcandidata-othercolbe']); + $saveFields($manualUrl, [ + 'formcandidata-othercola' => 'P16Zh
R&D', + 'formcandidata-othercolae' => 'P16En
R&D', + 'formcandidata-othercolb' => 'P16TextZh
R&D', + 'formcandidata-othercolbe' => 'P16TextEn
R&D', + ]); + $resultFields = explode(',', $originalResults['formresultsconfig-showfieldsort']); + $saveFields($resultUrl, ['formresultsconfig-showfieldsort' => implode(',', array_unique(array_merge($resultFields, ['otherColA', 'otherColB'])))]); + $admin->go($resultUrl); + $count = "[...document.querySelectorAll('button[type=submit]')].find(e => e.innerText.trim() === '開票')"; + if ($admin->js("!!($count)")) { + $admin->click($count); + $admin->waitLoad(); + } + foreach (['zh-TW' => 'Zh', 'en-US' => 'En'] as $language => $locale) { + foreach (['ballot-work/result', 'result/index', 'vote/result', 'vote/vote-detail', 'ballot/edit'] as $page) { + $extra = $page === 'ballot/edit' ? '&party=0&ballotID=1' : ''; + $status = $admin->go('/'.$page.'?voteID='.$vote.$extra.'&hl='.$language); + $r = $admin->js('(() => { + const a = [...document.querySelectorAll("td")].find(e => e.textContent.includes("P16'.$locale.'")); + const b = [...document.querySelectorAll("td")].find(e => e.textContent.includes("P16Text'.$locale.'")); + const h = [...document.querySelectorAll("th")].find(e => e.textContent.includes("P16Label'.$locale.'")); + const range = document.createRange(); let lineBreak = false; + if (a && a.querySelector("br") && a.querySelector("b")) { + range.selectNodeContents(a.querySelector("b")); + const bottom = range.getBoundingClientRect().top; + const text = [...a.childNodes].find(n => n.nodeType === Node.TEXT_NODE && n.textContent.includes("P16")); + if (text) { range.selectNodeContents(text); lineBreak = bottom > range.getBoundingClientRect().top; } + else { lineBreak = a.innerText.includes("\\n"); } + } + return {a: !!a && !!a.querySelector("br") && a.querySelector("b").textContent === "R&D", + lineBreak, header: !!h && !!h.querySelector("br") && h.querySelector("b").textContent === "R&D", + plain: !!b && !b.querySelector("br") && b.textContent.trim() === "P16Text'.$locale.'
R&D", + safe: !window.p16attack && ![...document.querySelectorAll("td [onclick]")].some(e => e.getAttribute("onclick").includes("p16attack"))}; + })()'); + $admin->check("$page $language: safe markup and plain text", $status === 200 && !in_array(false, $r, true), json_encode($r)); + $admin->shot('result-fields-'.str_replace('/', '-', $page).'-'.$language); + if (in_array($page, ['ballot-work/result', 'result/index'])) { + foreach (['csv' => 'exportTEXT', 'xls' => 'exportEXCEL', 'html' => 'exportHTML'] as $type => $method) { + $export = $admin->js('(() => { + const link = document.querySelector("a[data-format='.$type.']"); + const plugin = link && jQuery(link).data("gridexport"); + if (!plugin) throw new Error("Export plugin missing: '.$type.'"); + let out; const original = plugin.download; + try { plugin.download = (type, content) => { out = content; }; plugin.'.$method.'("'.$type.'"); } + finally { plugin.download = original; } + return out; + })()'); + $admin->check("$page $language: $type export content", is_string($export) && str_contains($export, 'P16'.$locale) && !str_contains($export, 'p16attack') && !str_contains($export, 'R&amp;D') && ($type !== 'csv' || str_contains($export, 'R&D'))); + $file('result-fields-'.str_replace('/', '-', $page).'-'.$language.'.'.$type, $export ?? ''); + } + } + } + } + $admin->go('/ballot/print?voteID='.$vote.'&questionID=1&hl=zh-TW'); + $admin->check('Printed ballot keeps both languages and safe breaks', $admin->js('document.body.innerText.includes("P16Zh") && document.body.innerText.includes("P16En") && !document.querySelector("b[onclick*=p16attack]") && !!document.querySelector("td br")')); + $admin->shot('result-fields-print'); +} finally { + // Restore values before removing the temporary fields from the configuration. + if ($candidateOriginal !== null) { + $saveFields($manualUrl, $candidateOriginal); + } + $saveFields($resultUrl, $originalResults); + $saveFields($configUrl, $originalConfig); +} diff --git a/tests/functional/ResultFieldRenderingCest.php b/tests/functional/ResultFieldRenderingCest.php index bf32371..023fa55 100644 --- a/tests/functional/ResultFieldRenderingCest.php +++ b/tests/functional/ResultFieldRenderingCest.php @@ -70,6 +70,8 @@ public function testRendering(FunctionalTester $I, Example $case): void $values[$field.$suffix] = 'R&D
'; } } + $values['Name'] = 'NZ
R&D'; + $values['NameE'] = 'NE
R&D'; if ($case['encoded']) { $values['otherColA'] = $values['otherColAE'] = 'Literal<br>R&D'; } @@ -135,6 +137,17 @@ public function testRendering(FunctionalTester $I, Example $case): void } } Assert::assertGreaterThan(0, $xpath->query('//td[normalize-space(.)="R&D
"]')->length, 'Plain text is encoded once'); + Assert::assertGreaterThan(0, $xpath->query('//th[normalize-space(.)="R&D"]')->length, 'Name label is encoded once'); + $names = $xpath->query('//td[contains(., "'.($locale === 'Zh' ? 'NZ' : 'NE').'")]'); + Assert::assertGreaterThan(0, $names->length); + $nameCell = $names->item(0); + if (in_array($case['page'], ['ballot-work/result', 'result/index', 'vote/result', 'selected'])) { + Assert::assertGreaterThan(0, $xpath->query('.//br', $nameCell)->length, 'Already purified names render once'); + Assert::assertSame('R&D', $xpath->query('.//b', $nameCell)->item(0)->textContent); + } else { + Assert::assertSame(0, $xpath->query('.//br | .//b', $nameCell)->length); + Assert::assertSame($values['Name'.($locale === 'Zh' ? '' : 'E')], trim($nameCell->textContent)); + } Assert::assertSame($before, FormResults::find()->where(['voteID' => $voteID])->orderBy('candID')->asArray()->all(), 'Rendering leaves counts, ranks and elected status unchanged'); } finally { Yii::$app->language = $originalLanguage; diff --git a/views/ballot-work/result.php b/views/ballot-work/result.php index 7a98973..32b5913 100644 --- a/views/ballot-work/result.php +++ b/views/ballot-work/result.php @@ -231,8 +231,7 @@ return ''; return Model::i18n( Yii::$app->params['ct.candi.jobLctnEAry'][$model->candiData->jobLctn], - Yii::$app->params['ct.candi.jobLctnAry'][$model->candiData->jobLctn] - ); + Yii::$app->params['ct.candi.jobLctnAry'][$model->candiData->jobLctn], null); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], @@ -248,10 +247,10 @@ { if(ArrayHelper::keyExists($model->candiData->instName,$instEAry)) { - return Model::i18n($instEAry[$model->candiData->instName],$model->candiData->instName); + return Model::i18n($instEAry[$model->candiData->instName],$model->candiData->instName, null); } } - return Model::i18n($model->candiData->instNameE, $model->candiData->instName); + return Model::i18n($model->candiData->instNameE, $model->candiData->instName, null); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], @@ -263,14 +262,16 @@ { if(trim($model->candiData->title) == '') return ''; - return Model::i18n($model->candiData->titleE, $model->candiData->title); + return Model::i18n($model->candiData->titleE, $model->candiData->title, null); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], ], 'Name' => [ + 'encodeLabel' => false, 'label' => Model::i18n($candiConfig->NameE, $candiConfig->Name), 'attribute' => 'Name', + 'format' => 'raw', 'value' => function ($model, $key, $index, $column) { return Model::i18n($model->candiData->NameE, $model->candiData->Name, false); @@ -287,8 +288,7 @@ return ''; return Model::i18n( Yii::$app->params['ct.candi.sexEAry'][$model->candiData->sex], - Yii::$app->params['ct.candi.sexAry'][$model->candiData->sex] - ); + Yii::$app->params['ct.candi.sexAry'][$model->candiData->sex], null); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], @@ -305,61 +305,68 @@ 'contentOptions' => ['class' => 'align-middle text-center'], ], 'otherColA' => [ - 'label' => Model::i18n($candiConfig->otherColNameAE, $candiConfig->otherColNameA), + 'encodeLabel' => false, + 'label' => Model::i18n($candiConfig->otherColNameAE, $candiConfig->otherColNameA, false), 'attribute' => Model::i18n('otherColAE', 'otherColA'), + 'format' => 'raw', 'value' => function ($model, $key, $index, $column) { - return Model::i18n($model->candiData->otherColAE, $model->candiData->otherColA); + return Model::i18n($model->candiData->otherColAE, $model->candiData->otherColA, false); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], ], 'otherColB' => [ - 'label' => Model::i18n($candiConfig->otherColNameBE, $candiConfig->otherColNameB), + 'encodeLabel' => false, + 'label' => Model::i18n($candiConfig->otherColNameBE, $candiConfig->otherColNameB, false), 'attribute' => Model::i18n('otherColBE', 'otherColB'), 'value' => function ($model, $key, $index, $column) { - return Model::i18n($model->candiData->otherColBE, $model->candiData->otherColB); + return Model::i18n($model->candiData->otherColBE, $model->candiData->otherColB, null); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], ], 'otherColC' => [ - 'label' => Model::i18n($candiConfig->otherColNameCE, $candiConfig->otherColNameC), + 'encodeLabel' => false, + 'label' => Model::i18n($candiConfig->otherColNameCE, $candiConfig->otherColNameC, false), 'attribute' => Model::i18n('otherColCE', 'otherColC'), 'value' => function ($model, $key, $index, $column) { - return Model::i18n($model->candiData->otherColCE, $model->candiData->otherColC); + return Model::i18n($model->candiData->otherColCE, $model->candiData->otherColC, null); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], ], 'otherColD' => [ - 'label' => Model::i18n($candiConfig->otherColNameDE, $candiConfig->otherColNameD), + 'encodeLabel' => false, + 'label' => Model::i18n($candiConfig->otherColNameDE, $candiConfig->otherColNameD, false), 'attribute' => Model::i18n('otherColDE', 'otherColD'), 'value' => function ($model, $key, $index, $column) { - return Model::i18n($model->candiData->otherColDE, $model->candiData->otherColD); + return Model::i18n($model->candiData->otherColDE, $model->candiData->otherColD, null); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], ], 'otherColE' => [ - 'label' => Model::i18n($candiConfig->otherColNameEE, $candiConfig->otherColNameE), + 'encodeLabel' => false, + 'label' => Model::i18n($candiConfig->otherColNameEE, $candiConfig->otherColNameE, false), 'attribute' => Model::i18n('otherColEE', 'otherColE'), 'value' => function ($model, $key, $index, $column) { - return Model::i18n($model->candiData->otherColEE, $model->candiData->otherColE); + return Model::i18n($model->candiData->otherColEE, $model->candiData->otherColE, null); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], ], 'otherColF' => [ - 'label' => Model::i18n($candiConfig->otherColNameFE, $candiConfig->otherColNameF), + 'encodeLabel' => false, + 'label' => Model::i18n($candiConfig->otherColNameFE, $candiConfig->otherColNameF, false), 'attribute' => Model::i18n('otherColFE', 'otherColF'), 'value' => function ($model, $key, $index, $column) { - return Model::i18n($model->candiData->otherColFE, $model->candiData->otherColF); + return Model::i18n($model->candiData->otherColFE, $model->candiData->otherColF, null); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], diff --git a/views/ballot/ballot.php b/views/ballot/ballot.php index a75dfd7..43351e3 100644 --- a/views/ballot/ballot.php +++ b/views/ballot/ballot.php @@ -207,8 +207,7 @@ { return Model::i18n( $parties[$model2->party], - $parties[$model2->party] - ); + $parties[$model2->party], null); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], @@ -222,8 +221,7 @@ return ''; return Model::i18n( Yii::$app->params['ct.candi.jobLctnEAry'][$model->jobLctn], - Yii::$app->params['ct.candi.jobLctnAry'][$model->jobLctn] - ); + Yii::$app->params['ct.candi.jobLctnAry'][$model->jobLctn], null); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], @@ -239,10 +237,10 @@ { if(ArrayHelper::keyExists($model->instName,$instEAry)) { - return Model::i18n($instEAry[$model->instName],$model->instName); + return Model::i18n($instEAry[$model->instName],$model->instName, null); } } - return Model::i18n($model->instNameE,$model->instName); + return Model::i18n($model->instNameE,$model->instName, null); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], @@ -254,17 +252,18 @@ { if(trim($model->title) == '') return ''; - return Model::i18n($model->titleE,$model->title); + return Model::i18n($model->titleE,$model->title, null); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], ], 'Name' => [ + 'encodeLabel' => false, 'label' => Model::i18n($candiConfig->NameE, $candiConfig->Name), 'attribute' => 'Name', 'value' => function ($model, $key, $index, $column) { - return Model::i18n($model->NameE,$model->Name); + return Model::i18n($model->NameE,$model->Name, null); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], @@ -278,8 +277,7 @@ return ''; return Model::i18n( Yii::$app->params['ct.candi.sexEAry'][$model->sex], - Yii::$app->params['ct.candi.sexAry'][$model->sex] - ); + Yii::$app->params['ct.candi.sexAry'][$model->sex], null); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], @@ -295,37 +293,47 @@ 'contentOptions' => ['class' => 'align-middle text-center'], ], 'otherColA' => [ - 'label' => Model::i18n($candiConfig->otherColNameAE, $candiConfig->otherColNameA), + 'encodeLabel' => false, + 'label' => Model::i18n($candiConfig->otherColNameAE, $candiConfig->otherColNameA, false), 'attribute' => Model::i18n('otherColAE', 'otherColA'), + 'format' => 'raw', + 'value' => function ($model) { + return Model::i18n($model->otherColAE, $model->otherColA, false); + }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], ], 'otherColB' => [ - 'label' => Model::i18n($candiConfig->otherColNameBE, $candiConfig->otherColNameB), + 'encodeLabel' => false, + 'label' => Model::i18n($candiConfig->otherColNameBE, $candiConfig->otherColNameB, false), 'attribute' => Model::i18n('otherColBE', 'otherColB'), 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], ], 'otherColC' => [ - 'label' => Model::i18n($candiConfig->otherColNameCE, $candiConfig->otherColNameC), + 'encodeLabel' => false, + 'label' => Model::i18n($candiConfig->otherColNameCE, $candiConfig->otherColNameC, false), 'attribute' => Model::i18n('otherColCE', 'otherColC'), 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], ], 'otherColD' => [ - 'label' => Model::i18n($candiConfig->otherColNameDE, $candiConfig->otherColNameD), + 'encodeLabel' => false, + 'label' => Model::i18n($candiConfig->otherColNameDE, $candiConfig->otherColNameD, false), 'attribute' => Model::i18n('otherColDE', 'otherColD'), 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], ], 'otherColE' => [ - 'label' => Model::i18n($candiConfig->otherColNameEE, $candiConfig->otherColNameE), + 'encodeLabel' => false, + 'label' => Model::i18n($candiConfig->otherColNameEE, $candiConfig->otherColNameE, false), 'attribute' => Model::i18n('otherColEE', 'otherColE'), 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], ], 'otherColF' => [ - 'label' => Model::i18n($candiConfig->otherColNameFE, $candiConfig->otherColNameF), + 'encodeLabel' => false, + 'label' => Model::i18n($candiConfig->otherColNameFE, $candiConfig->otherColNameF, false), 'attribute' => Model::i18n('otherColFE', 'otherColF'), 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], diff --git a/views/result/index.php b/views/result/index.php index 170084d..a6f99e2 100644 --- a/views/result/index.php +++ b/views/result/index.php @@ -231,8 +231,7 @@ return ''; return Model::i18n( Yii::$app->params['ct.candi.jobLctnEAry'][$model->candiData->jobLctn], - Yii::$app->params['ct.candi.jobLctnAry'][$model->candiData->jobLctn] - ); + Yii::$app->params['ct.candi.jobLctnAry'][$model->candiData->jobLctn], null); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], @@ -251,10 +250,10 @@ { if(ArrayHelper::keyExists($model->candiData->instName,$instEAry)) { - return Model::i18n($instEAry[$model->candiData->instName],$model->candiData->instName); + return Model::i18n($instEAry[$model->candiData->instName],$model->candiData->instName, null); } } - return Model::i18n($model->candiData->instNameE, $model->candiData->instName); + return Model::i18n($model->candiData->instNameE, $model->candiData->instName, null); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], @@ -266,14 +265,16 @@ { if(trim($model->candiData->title ?? '') == '') return ''; - return Model::i18n($model->candiData->titleE, $model->candiData->title); + return Model::i18n($model->candiData->titleE, $model->candiData->title, null); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], ], 'Name' => [ + 'encodeLabel' => false, 'label' => Model::i18n($candiConfig->NameE, $candiConfig->Name), 'attribute' => 'Name', + 'format' => 'raw', 'value' => function ($model, $key, $index, $column) { return Model::i18n($model->candiData->NameE ?? '', $model->candiData->Name ?? '', false); @@ -290,8 +291,7 @@ return ''; return Model::i18n( Yii::$app->params['ct.candi.sexEAry'][$model->candiData->sex ?? ''], - Yii::$app->params['ct.candi.sexAry'][$model->candiData->sex ?? ''] - ); + Yii::$app->params['ct.candi.sexAry'][$model->candiData->sex ?? ''], null); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], @@ -308,61 +308,68 @@ 'contentOptions' => ['class' => 'align-middle text-center'], ], 'otherColA' => [ - 'label' => Model::i18n($candiConfig->otherColNameAE ?? '', $candiConfig->otherColNameA ?? ''), + 'encodeLabel' => false, + 'label' => Model::i18n($candiConfig->otherColNameAE ?? '', $candiConfig->otherColNameA ?? '', false), 'attribute' => Model::i18n('otherColAE', 'otherColA'), + 'format' => 'raw', 'value' => function ($model, $key, $index, $column) { - return Model::i18n($model->candiData->otherColAE ?? '', $model->candiData->otherColA ?? ''); + return Model::i18n($model->candiData->otherColAE ?? '', $model->candiData->otherColA ?? '', false); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], ], 'otherColB' => [ - 'label' => Model::i18n($candiConfig->otherColNameBE ?? '', $candiConfig->otherColNameB ?? ''), + 'encodeLabel' => false, + 'label' => Model::i18n($candiConfig->otherColNameBE ?? '', $candiConfig->otherColNameB ?? '', false), 'attribute' => Model::i18n('otherColBE', 'otherColB'), 'value' => function ($model, $key, $index, $column) { - return Model::i18n($model->candiData->otherColBE ?? '', $model->candiData->otherColB ?? ''); + return Model::i18n($model->candiData->otherColBE ?? '', $model->candiData->otherColB ?? '', null); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], ], 'otherColC' => [ - 'label' => Model::i18n($candiConfig->otherColNameCE ?? '', $candiConfig->otherColNameC ?? ''), + 'encodeLabel' => false, + 'label' => Model::i18n($candiConfig->otherColNameCE ?? '', $candiConfig->otherColNameC ?? '', false), 'attribute' => Model::i18n('otherColCE', 'otherColC'), 'value' => function ($model, $key, $index, $column) { - return Model::i18n($model->candiData->otherColCE ?? '', $model->candiData->otherColC ?? ''); + return Model::i18n($model->candiData->otherColCE ?? '', $model->candiData->otherColC ?? '', null); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], ], 'otherColD' => [ - 'label' => Model::i18n($candiConfig->otherColNameDE ?? '', $candiConfig->otherColNameD ?? ''), + 'encodeLabel' => false, + 'label' => Model::i18n($candiConfig->otherColNameDE ?? '', $candiConfig->otherColNameD ?? '', false), 'attribute' => Model::i18n('otherColDE', 'otherColD'), 'value' => function ($model, $key, $index, $column) { - return Model::i18n($model->candiData->otherColDE ?? '', $model->candiData->otherColD ?? ''); + return Model::i18n($model->candiData->otherColDE ?? '', $model->candiData->otherColD ?? '', null); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], ], 'otherColE' => [ - 'label' => Model::i18n($candiConfig->otherColNameEE ?? '', $candiConfig->otherColNameE ?? '' ), + 'encodeLabel' => false, + 'label' => Model::i18n($candiConfig->otherColNameEE ?? '', $candiConfig->otherColNameE ?? '', false), 'attribute' => Model::i18n('otherColEE', 'otherColE'), 'value' => function ($model, $key, $index, $column) { - return Model::i18n($model->candiData->otherColEE ?? '', $model->candiData->otherColE ?? ''); + return Model::i18n($model->candiData->otherColEE ?? '', $model->candiData->otherColE ?? '', null); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], ], 'otherColF' => [ - 'label' => Model::i18n($candiConfig->otherColNameFE, $candiConfig->otherColNameF), + 'encodeLabel' => false, + 'label' => Model::i18n($candiConfig->otherColNameFE, $candiConfig->otherColNameF, false), 'attribute' => Model::i18n('otherColFE', 'otherColF'), 'value' => function ($model, $key, $index, $column) { - return Model::i18n($model->candiData->otherColFE ?? '', $model->candiData->otherColF ?? ''); + return Model::i18n($model->candiData->otherColFE ?? '', $model->candiData->otherColF ?? '', null); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], diff --git a/views/vote/_ballot-selected.php b/views/vote/_ballot-selected.php index a653992..e49a088 100644 --- a/views/vote/_ballot-selected.php +++ b/views/vote/_ballot-selected.php @@ -80,8 +80,7 @@ return ''; return Model::i18n( Yii::$app->params['ct.candi.jobLctnEAry'][$model->candiData->jobLctn], - Yii::$app->params['ct.candi.jobLctnAry'][$model->candiData->jobLctn] - ); + Yii::$app->params['ct.candi.jobLctnAry'][$model->candiData->jobLctn], null); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], @@ -97,10 +96,10 @@ { if(ArrayHelper::keyExists($model->candiData->instName,$instEAry)) { - return Model::i18n($instEAry[$model->candiData->instName],$model->candiData->instName); + return Model::i18n($instEAry[$model->candiData->instName],$model->candiData->instName, null); } } - return Model::i18n($model->candiData->instNameE,$model->candiData->instName); + return Model::i18n($model->candiData->instNameE,$model->candiData->instName, null); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center text-nowrap text-wrap'], @@ -110,16 +109,18 @@ 'attribute' => 'title', 'value' => function ($model, $key, $index, $column) { - if(trim(Model::i18n($model->candiData->titleE,$model->candiData->title)) == '') + if(trim(Model::i18n($model->candiData->titleE,$model->candiData->title, null)) == '') return ''; - return Model::i18n($model->candiData->titleE,$model->candiData->title); + return Model::i18n($model->candiData->titleE,$model->candiData->title, null); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center text-nowrap text-wrap'], ], 'Name' => [ + 'encodeLabel' => false, 'label' => Model::i18n($candiConfig->NameE, $candiConfig->Name), 'attribute' => 'Name', + 'format' => 'raw', 'value' => function ($model, $key, $index, $column) { return Model::i18n($model->candiData->NameE, $model->candiData->Name, false); @@ -136,8 +137,7 @@ return ''; return Model::i18n( Yii::$app->params['ct.candi.sexEAry'][$model->candiData->sex], - Yii::$app->params['ct.candi.sexAry'][$model->candiData->sex] - ); + Yii::$app->params['ct.candi.sexAry'][$model->candiData->sex], null); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], @@ -154,61 +154,68 @@ 'contentOptions' => ['class' => 'align-middle text-center'], ], 'otherColA' => [ - 'label' => Model::i18n($candiConfig->otherColNameAE, $candiConfig->otherColNameA), + 'encodeLabel' => false, + 'label' => Model::i18n($candiConfig->otherColNameAE, $candiConfig->otherColNameA, false), 'attribute' => Model::i18n('otherColAE', 'otherColA'), + 'format' => 'raw', 'value' => function ($model, $key, $index, $column) { - return Model::i18n($model->candiData->otherColAE, $model->candiData->otherColA); + return Model::i18n($model->candiData->otherColAE, $model->candiData->otherColA, false); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], ], 'otherColB' => [ - 'label' => Model::i18n($candiConfig->otherColNameBE, $candiConfig->otherColNameB), + 'encodeLabel' => false, + 'label' => Model::i18n($candiConfig->otherColNameBE, $candiConfig->otherColNameB, false), 'attribute' => Model::i18n('otherColBE', 'otherColB'), 'value' => function ($model, $key, $index, $column) { - return Model::i18n($model->candiData->otherColBE, $model->candiData->otherColB); + return Model::i18n($model->candiData->otherColBE, $model->candiData->otherColB, null); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], ], 'otherColC' => [ - 'label' => Model::i18n($candiConfig->otherColNameCE, $candiConfig->otherColNameC), + 'encodeLabel' => false, + 'label' => Model::i18n($candiConfig->otherColNameCE, $candiConfig->otherColNameC, false), 'attribute' => Model::i18n('otherColCE', 'otherColC'), 'value' => function ($model, $key, $index, $column) { - return Model::i18n($model->candiData->otherColCE, $model->candiData->otherColC); + return Model::i18n($model->candiData->otherColCE, $model->candiData->otherColC, null); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], ], 'otherColD' => [ - 'label' => Model::i18n($candiConfig->otherColNameDE, $candiConfig->otherColNameD), + 'encodeLabel' => false, + 'label' => Model::i18n($candiConfig->otherColNameDE, $candiConfig->otherColNameD, false), 'attribute' => Model::i18n('otherColDE', 'otherColD'), 'value' => function ($model, $key, $index, $column) { - return Model::i18n($model->candiData->otherColDE, $model->candiData->otherColD); + return Model::i18n($model->candiData->otherColDE, $model->candiData->otherColD, null); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], ], 'otherColE' => [ - 'label' => Model::i18n($candiConfig->otherColNameEE, $candiConfig->otherColNameE), + 'encodeLabel' => false, + 'label' => Model::i18n($candiConfig->otherColNameEE, $candiConfig->otherColNameE, false), 'attribute' => Model::i18n('otherColEE', 'otherColE'), 'value' => function ($model, $key, $index, $column) { - return Model::i18n($model->candiData->otherColEE, $model->candiData->otherColE); + return Model::i18n($model->candiData->otherColEE, $model->candiData->otherColE, null); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], ], 'otherColF' => [ - 'label' => Model::i18n($candiConfig->otherColNameFE, $candiConfig->otherColNameF), + 'encodeLabel' => false, + 'label' => Model::i18n($candiConfig->otherColNameFE, $candiConfig->otherColNameF, false), 'attribute' => Model::i18n('otherColFE', 'otherColF'), 'value' => function ($model, $key, $index, $column) { - return Model::i18n($model->candiData->otherColFE, $model->candiData->otherColF); + return Model::i18n($model->candiData->otherColFE, $model->candiData->otherColF, null); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], diff --git a/views/vote/_candi-list.php b/views/vote/_candi-list.php index 9338429..246e273 100644 --- a/views/vote/_candi-list.php +++ b/views/vote/_candi-list.php @@ -73,8 +73,7 @@ return ''; return Model::i18n( Yii::$app->params['ct.candi.jobLctnEAry'][$model->jobLctn], - Yii::$app->params['ct.candi.jobLctnAry'][$model->jobLctn] - ); + Yii::$app->params['ct.candi.jobLctnAry'][$model->jobLctn], null); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], @@ -90,10 +89,10 @@ { if(ArrayHelper::keyExists($model->instName,$instEAry)) { - return Model::i18n($instEAry[$model->instName],$model->instName); + return Model::i18n($instEAry[$model->instName],$model->instName, null); } } - return Model::i18n($model->instNameE,$model->instName); + return Model::i18n($model->instNameE,$model->instName, null); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], @@ -105,17 +104,18 @@ { if(trim($model->title) == '') return ''; - return Model::i18n($model->titleE,$model->title); + return Model::i18n($model->titleE,$model->title, null); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], ], 'Name' => [ + 'encodeLabel' => false, 'label' => Model::i18n($candiConfig->NameE, $candiConfig->Name), 'attribute' => 'Name', 'value' => function ($model, $key, $index, $column) { - return Model::i18n($model->NameE,$model->Name); + return Model::i18n($model->NameE,$model->Name, null); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center text-nowrap'], @@ -129,8 +129,7 @@ return ''; return Model::i18n( Yii::$app->params['ct.candi.sexEAry'][$model->sex], - Yii::$app->params['ct.candi.sexAry'][$model->sex] - ); + Yii::$app->params['ct.candi.sexAry'][$model->sex], null); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], @@ -146,7 +145,8 @@ 'contentOptions' => ['class' => 'align-middle text-center'], ], 'otherColA' => [ - 'label' => Model::i18n($candiConfig->otherColNameAE, $candiConfig->otherColNameA), + 'encodeLabel' => false, + 'label' => Model::i18n($candiConfig->otherColNameAE, $candiConfig->otherColNameA, false), 'attribute' => Model::i18n('otherColAE', 'otherColA'), 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => [ @@ -161,31 +161,36 @@ }, ], 'otherColB' => [ - 'label' => Model::i18n($candiConfig->otherColNameBE, $candiConfig->otherColNameB), + 'encodeLabel' => false, + 'label' => Model::i18n($candiConfig->otherColNameBE, $candiConfig->otherColNameB, false), 'attribute' => Model::i18n('otherColBE', 'otherColB'), 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], ], 'otherColC' => [ - 'label' => Model::i18n($candiConfig->otherColNameCE, $candiConfig->otherColNameC), + 'encodeLabel' => false, + 'label' => Model::i18n($candiConfig->otherColNameCE, $candiConfig->otherColNameC, false), 'attribute' => Model::i18n('otherColCE', 'otherColC'), 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], ], 'otherColD' => [ - 'label' => Model::i18n($candiConfig->otherColNameDE, $candiConfig->otherColNameD), + 'encodeLabel' => false, + 'label' => Model::i18n($candiConfig->otherColNameDE, $candiConfig->otherColNameD, false), 'attribute' => Model::i18n('otherColDE', 'otherColD'), 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], ], 'otherColE' => [ - 'label' => Model::i18n($candiConfig->otherColNameEE, $candiConfig->otherColNameE), + 'encodeLabel' => false, + 'label' => Model::i18n($candiConfig->otherColNameEE, $candiConfig->otherColNameE, false), 'attribute' => Model::i18n('otherColEE', 'otherColE'), 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], ], 'otherColF' => [ - 'label' => Model::i18n($candiConfig->otherColNameFE, $candiConfig->otherColNameF), + 'encodeLabel' => false, + 'label' => Model::i18n($candiConfig->otherColNameFE, $candiConfig->otherColNameF, false), 'attribute' => Model::i18n('otherColFE', 'otherColF'), 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], diff --git a/views/vote/result.php b/views/vote/result.php index 4c9461c..19595cb 100644 --- a/views/vote/result.php +++ b/views/vote/result.php @@ -132,8 +132,7 @@ return ''; return Model::i18n( Yii::$app->params['ct.candi.jobLctnEAry'][$model->candiData->jobLctn], - Yii::$app->params['ct.candi.jobLctnAry'][$model->candiData->jobLctn] - ); + Yii::$app->params['ct.candi.jobLctnAry'][$model->candiData->jobLctn], null); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], @@ -149,10 +148,10 @@ { if(ArrayHelper::keyExists($model->candiData->instName,$instEAry)) { - return Model::i18n($instEAry[$model->candiData->instName],$model->candiData->instName); + return Model::i18n($instEAry[$model->candiData->instName],$model->candiData->instName, null); } } - return Model::i18n($model->candiData->instNameE,$model->candiData->instName); + return Model::i18n($model->candiData->instNameE,$model->candiData->instName, null); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], @@ -164,14 +163,16 @@ { if(trim($model->candiData->title ?? '') == '') return ''; - return Model::i18n($model->candiData->titleE,$model->candiData->title); + return Model::i18n($model->candiData->titleE,$model->candiData->title, null); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], ], 'Name' => [ + 'encodeLabel' => false, 'label' => Model::i18n($candiConfig->NameE, $candiConfig->Name), 'attribute' => 'Name', + 'format' => 'raw', 'value' => function ($model, $key, $index, $column) { return Model::i18n($model->candiData->NameE ?? '', $model->candiData->Name ?? '', false); @@ -188,8 +189,7 @@ return ''; return Model::i18n( Yii::$app->params['ct.candi.sexEAry'][$model->candiData->sex ?? ''], - Yii::$app->params['ct.candi.sexAry'][$model->candiData->sex ?? ''] - ); + Yii::$app->params['ct.candi.sexAry'][$model->candiData->sex ?? ''], null); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], @@ -206,61 +206,68 @@ 'contentOptions' => ['class' => 'align-middle text-center'], ], 'otherColA' => [ - 'label' => Model::i18n($candiConfig->otherColNameAE ?? '', $candiConfig->otherColNameA ?? ''), + 'encodeLabel' => false, + 'label' => Model::i18n($candiConfig->otherColNameAE ?? '', $candiConfig->otherColNameA ?? '', false), 'attribute' => Model::i18n('otherColAE', 'otherColA'), + 'format' => 'raw', 'value' => function ($model, $key, $index, $column) { - return Model::i18n($model->candiData->otherColAE ?? '', $model->candiData->otherColA ?? ''); + return Model::i18n($model->candiData->otherColAE ?? '', $model->candiData->otherColA ?? '', false); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], ], 'otherColB' => [ - 'label' => Model::i18n($candiConfig->otherColNameBE ?? '', $candiConfig->otherColNameB ?? ''), + 'encodeLabel' => false, + 'label' => Model::i18n($candiConfig->otherColNameBE ?? '', $candiConfig->otherColNameB ?? '', false), 'attribute' => Model::i18n('otherColBE', 'otherColB'), 'value' => function ($model, $key, $index, $column) { - return Model::i18n($model->candiData->otherColBE ?? '', $model->candiData->otherColB ?? ''); + return Model::i18n($model->candiData->otherColBE ?? '', $model->candiData->otherColB ?? '', null); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], ], 'otherColC' => [ - 'label' => Model::i18n($candiConfig->otherColNameCE ?? '', $candiConfig->otherColNameC ?? ''), + 'encodeLabel' => false, + 'label' => Model::i18n($candiConfig->otherColNameCE ?? '', $candiConfig->otherColNameC ?? '', false), 'attribute' => Model::i18n('otherColCE', 'otherColC'), 'value' => function ($model, $key, $index, $column) { - return Model::i18n($model->candiData->otherColCE ?? '', $model->candiData->otherColC ?? ''); + return Model::i18n($model->candiData->otherColCE ?? '', $model->candiData->otherColC ?? '', null); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], ], 'otherColD' => [ - 'label' => Model::i18n($candiConfig->otherColNameDE ?? '', $candiConfig->otherColNameD ?? ''), + 'encodeLabel' => false, + 'label' => Model::i18n($candiConfig->otherColNameDE ?? '', $candiConfig->otherColNameD ?? '', false), 'attribute' => Model::i18n('otherColDE', 'otherColD'), 'value' => function ($model, $key, $index, $column) { - return Model::i18n($model->candiData->otherColDE ?? '', $model->candiData->otherColD ?? ''); + return Model::i18n($model->candiData->otherColDE ?? '', $model->candiData->otherColD ?? '', null); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], ], 'otherColE' => [ - 'label' => Model::i18n($candiConfig->otherColNameEE ?? '', $candiConfig->otherColNameE ?? ''), + 'encodeLabel' => false, + 'label' => Model::i18n($candiConfig->otherColNameEE ?? '', $candiConfig->otherColNameE ?? '', false), 'attribute' => Model::i18n('otherColEE', 'otherColE'), 'value' => function ($model, $key, $index, $column) { - return Model::i18n($model->candiData->otherColEE, $model->candiData->otherColE); + return Model::i18n($model->candiData->otherColEE, $model->candiData->otherColE, null); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], ], 'otherColF' => [ - 'label' => Model::i18n($candiConfig->otherColNameFE ?? '', $candiConfig->otherColNameF ?? ''), + 'encodeLabel' => false, + 'label' => Model::i18n($candiConfig->otherColNameFE ?? '', $candiConfig->otherColNameF ?? '', false), 'attribute' => Model::i18n('otherColFE', 'otherColF'), 'value' => function ($model, $key, $index, $column) { - return Model::i18n($model->candiData->otherColFE ?? '', $model->candiData->otherColF ?? ''); + return Model::i18n($model->candiData->otherColFE ?? '', $model->candiData->otherColF ?? '', null); }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], From 37da2971ed1837cacb775d824904b4d1a300af41 Mon Sep 17 00:00:00 2001 From: TUNG-YEN Date: Fri, 6 Nov 2026 08:24:25 +0800 Subject: [PATCH 3/8] Use rendering fixtures independently of voter authentication --- tests/e2e/run.php | 2 +- tests/e2e/suites/result-fields.php | 5 +++++ tests/functional/ResultFieldRenderingCest.php | 6 +++++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/e2e/run.php b/tests/e2e/run.php index 8e164b8..5abd5c8 100644 --- a/tests/e2e/run.php +++ b/tests/e2e/run.php @@ -7,7 +7,7 @@ * --base 受測網站(預設 E2E_BASE_URL 或 http://localhost:8080),須為測試入口並已載入 fixture * --cdp 既有 Chrome 遠端除錯位址(預設 E2E_CDP_URL 或 http://127.0.0.1:9222);無法連線時自動啟動 Chrome * --chrome Chrome/Edge 執行檔(預設 E2E_CHROME 或自動偵測) - * --suite 只跑指定組別:pages, header, modals, attachments, photos, results, voting, anon, layout + * --suite 只跑指定組別:pages, header, modals, attachments, photos, results, result-fields, voting, anon, layout * * 結束代碼:全部通過為 0,否則為 1。截圖與 results.json 輸出至 tests/_output/e2e/ */ diff --git a/tests/e2e/suites/result-fields.php b/tests/e2e/suites/result-fields.php index d27aa34..5284fa5 100644 --- a/tests/e2e/suites/result-fields.php +++ b/tests/e2e/suites/result-fields.php @@ -54,6 +54,11 @@ $admin->click($count); $admin->waitLoad(); } + // Administrative login and voter login are independent sessions in the app. + $admin->go('/site/password?voteID='.$vote); + $admin->js('document.querySelector("#passwd").value = "testN90"'); + $admin->click("[...document.querySelectorAll('button[type=submit]')].find(e => e.innerText.includes('登入'))"); + $admin->waitLoad(); foreach (['zh-TW' => 'Zh', 'en-US' => 'En'] as $language => $locale) { foreach (['ballot-work/result', 'result/index', 'vote/result', 'vote/vote-detail', 'ballot/edit'] as $page) { $extra = $page === 'ballot/edit' ? '&party=0&ballotID=1' : ''; diff --git a/tests/functional/ResultFieldRenderingCest.php b/tests/functional/ResultFieldRenderingCest.php index 023fa55..ee3fb96 100644 --- a/tests/functional/ResultFieldRenderingCest.php +++ b/tests/functional/ResultFieldRenderingCest.php @@ -5,6 +5,7 @@ use app\models\FormManageVote; use app\models\FormResults; use app\models\ResultsConfig; +use app\models\Votes; use Codeception\Example; use PHPUnit\Framework\Assert; use yii\data\ActiveDataProvider; @@ -84,6 +85,8 @@ public function testRendering(FunctionalTester $I, Example $case): void $results = new FormResults(); $results->deleteResults($voteID, 1); Assert::assertTrue($results->createResults($voteID)); + // Keep this rendering test independent of anonymous login policy. + Votes::findOne(['voteID' => $voteID])->updateAttributes(['type' => Votes::TYPE_NO_AUTH]); $before = FormResults::find()->where(['voteID' => $voteID])->orderBy('candID')->asArray()->all(); Yii::$app->language = $case['language']; Yii::$app->session->set('language', $case['language']); @@ -111,7 +114,8 @@ public function testRendering(FunctionalTester $I, Example $case): void libxml_use_internal_errors($previous); } $xpath = new DOMXPath($dom); - Assert::assertSame(0, $xpath->query('//script[contains(., "p16attack()")] | //*[@onclick="p16attack()" or @onerror="p16attack()"]')->length); + // Candidate strings may appear as inert JSON inside the page's own scripts. + Assert::assertSame(0, $xpath->query('//script[normalize-space(.)="p16attack()"] | //*[@onclick="p16attack()" or @onerror="p16attack()"]')->length); $locale = $case['language'] === 'zh-TW' ? 'Zh' : 'En'; foreach (range('A', 'F') as $letter) { $headers = $xpath->query('//th[contains(., "L'.$letter.$locale.'")]'); From a8008f7ab908e9cda403a359b75db0182096a452 Mon Sep 17 00:00:00 2001 From: TUNG-YEN Date: Fri, 6 Nov 2026 08:29:20 +0800 Subject: [PATCH 4/8] Remove redundant Yii import exposed by voter result coverage --- tests/functional/ResultFieldRenderingCest.php | 4 +++- views/vote/result.php | 4 +--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/functional/ResultFieldRenderingCest.php b/tests/functional/ResultFieldRenderingCest.php index ee3fb96..5b5d55f 100644 --- a/tests/functional/ResultFieldRenderingCest.php +++ b/tests/functional/ResultFieldRenderingCest.php @@ -86,7 +86,9 @@ public function testRendering(FunctionalTester $I, Example $case): void $results->deleteResults($voteID, 1); Assert::assertTrue($results->createResults($voteID)); // Keep this rendering test independent of anonymous login policy. - Votes::findOne(['voteID' => $voteID])->updateAttributes(['type' => Votes::TYPE_NO_AUTH]); + if (in_array($case['page'], ['vote/result', 'selected'])) { + Votes::findOne(['voteID' => $voteID])->updateAttributes(['type' => Votes::TYPE_NO_AUTH]); + } $before = FormResults::find()->where(['voteID' => $voteID])->orderBy('candID')->asArray()->all(); Yii::$app->language = $case['language']; Yii::$app->session->set('language', $case['language']); diff --git a/views/vote/result.php b/views/vote/result.php index 19595cb..c9f16a8 100644 --- a/views/vote/result.php +++ b/views/vote/result.php @@ -1,6 +1,4 @@ render('@app/views/partials/print-mode'); \ No newline at end of file +echo $this->render('@app/views/partials/print-mode'); From 71fa10a3b4981310568f06fc0e035c31a2713cc8 Mon Sep 17 00:00:00 2001 From: TUNG-YEN Date: Fri, 6 Nov 2026 08:38:03 +0800 Subject: [PATCH 5/8] Select visible result fixtures and actual export controls in browser tests --- tests/e2e/suites/result-fields.php | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/tests/e2e/suites/result-fields.php b/tests/e2e/suites/result-fields.php index 5284fa5..9f57f91 100644 --- a/tests/e2e/suites/result-fields.php +++ b/tests/e2e/suites/result-fields.php @@ -2,8 +2,6 @@ /** Result rendering and export generation, using only fixture data and UI forms. */ $admin->section('result-fields'); $vote = 'AnonPartyTest'; -$configUrl = "/candi/config?voteID=$vote&questionID=1&hl=zh-TW"; -$manualUrl = "/candi/manual?voteID=$vote&action=update&id=1&hl=zh-TW"; $resultUrl = "/count/result?voteID=$vote&hl=zh-TW"; $readFields = function ($url, $ids) use ($admin) { $admin->go($url); @@ -24,8 +22,22 @@ throw new RuntimeException('Form validation failed: '.implode(' | ', $admin->flashes())); } }; -$originalConfig = $readFields($configUrl, ['formcandiconfig-showfieldsort', 'formcandiconfig-othercolnamea', 'formcandiconfig-othercolnameae']); $originalResults = $readFields($resultUrl, ['formresultsconfig-showfieldsort']); +$count = "[...document.querySelectorAll('button[type=submit]')].find(e => e.innerText.trim() === '開票')"; +if ($admin->js("!!($count)")) { + $admin->click($count); + $admin->waitLoad(); +} +$admin->go('/ballot-work/result?voteID='.$vote.'&hl=zh-TW'); +$candidateId = $admin->js('document.querySelector("tbody tr[data-key]")?.getAttribute("data-key")'); +if (!$candidateId) { + throw new RuntimeException('No visible result candidate'); +} +$manualUrl = "/candi/manual?voteID=$vote&action=update&id=$candidateId&hl=zh-TW"; +$admin->go($manualUrl); +$questionId = $admin->js('document.querySelector("#formcandidata-questionid").value'); +$configUrl = "/candi/config?voteID=$vote&questionID=$questionId&hl=zh-TW"; +$originalConfig = $readFields($configUrl, ['formcandiconfig-showfieldsort', 'formcandiconfig-othercolnamea', 'formcandiconfig-othercolnameae']); $candidateOriginal = null; try { $fields = json_decode($originalConfig['formcandiconfig-showfieldsort'], true); @@ -46,6 +58,10 @@ 'formcandidata-othercolb' => 'P16TextZh
R&D', 'formcandidata-othercolbe' => 'P16TextEn
R&D', ]); + $saved = $readFields($manualUrl, ['formcandidata-othercola', 'formcandidata-othercolb']); + if (!str_starts_with($saved['formcandidata-othercola'], 'P16Zh') || !str_starts_with($saved['formcandidata-othercolb'], 'P16TextZh')) { + throw new RuntimeException('Candidate markup was not saved'); + } $resultFields = explode(',', $originalResults['formresultsconfig-showfieldsort']); $saveFields($resultUrl, ['formresultsconfig-showfieldsort' => implode(',', array_unique(array_merge($resultFields, ['otherColA', 'otherColB'])))]); $admin->go($resultUrl); @@ -85,7 +101,7 @@ if (in_array($page, ['ballot-work/result', 'result/index'])) { foreach (['csv' => 'exportTEXT', 'xls' => 'exportEXCEL', 'html' => 'exportHTML'] as $type => $method) { $export = $admin->js('(() => { - const link = document.querySelector("a[data-format='.$type.']"); + const link = document.querySelector("a.export-'.$type.'"); const plugin = link && jQuery(link).data("gridexport"); if (!plugin) throw new Error("Export plugin missing: '.$type.'"); let out; const original = plugin.download; @@ -99,7 +115,7 @@ } } } - $admin->go('/ballot/print?voteID='.$vote.'&questionID=1&hl=zh-TW'); + $admin->go('/ballot/print?voteID='.$vote.'&questionID='.$questionId.'&hl=zh-TW'); $admin->check('Printed ballot keeps both languages and safe breaks', $admin->js('document.body.innerText.includes("P16Zh") && document.body.innerText.includes("P16En") && !document.querySelector("b[onclick*=p16attack]") && !!document.querySelector("td br")')); $admin->shot('result-fields-print'); } finally { From 29fc2f3b8a3e4309f8049bb6735d75c1d1d99f8b Mon Sep 17 00:00:00 2001 From: TUNG-YEN Date: Fri, 6 Nov 2026 08:42:09 +0800 Subject: [PATCH 6/8] Cover shared and question-specific labels in result browser checks --- tests/e2e/suites/result-fields.php | 33 ++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/tests/e2e/suites/result-fields.php b/tests/e2e/suites/result-fields.php index 9f57f91..8c7e01c 100644 --- a/tests/e2e/suites/result-fields.php +++ b/tests/e2e/suites/result-fields.php @@ -37,20 +37,26 @@ $admin->go($manualUrl); $questionId = $admin->js('document.querySelector("#formcandidata-questionid").value'); $configUrl = "/candi/config?voteID=$vote&questionID=$questionId&hl=zh-TW"; -$originalConfig = $readFields($configUrl, ['formcandiconfig-showfieldsort', 'formcandiconfig-othercolnamea', 'formcandiconfig-othercolnameae']); +$originalConfigs = []; +// Result grids use the first question's labels; candidate lists and print use their own question. +foreach (array_unique([$configUrl, "/candi/config?voteID=$vote&questionID=1&hl=zh-TW"]) as $url) { + $originalConfigs[$url] = $readFields($url, ['formcandiconfig-showfieldsort', 'formcandiconfig-othercolnamea', 'formcandiconfig-othercolnameae']); +} $candidateOriginal = null; try { - $fields = json_decode($originalConfig['formcandiconfig-showfieldsort'], true); - foreach (['otherColA', 'otherColB'] as $field) { - if (!in_array($field, array_column($fields, 'id'))) { - $fields[] = ['id' => $field, 'children' => []]; + foreach ($originalConfigs as $url => $originalConfig) { + $fields = json_decode($originalConfig['formcandiconfig-showfieldsort'], true); + foreach (['otherColA', 'otherColB'] as $field) { + if (!in_array($field, array_column($fields, 'id'))) { + $fields[] = ['id' => $field, 'children' => []]; + } } + $saveFields($url, [ + 'formcandiconfig-showfieldsort' => json_encode($fields), + 'formcandiconfig-othercolnamea' => 'P16LabelZh
R&D', + 'formcandiconfig-othercolnameae' => 'P16LabelEn
R&D', + ]); } - $saveFields($configUrl, [ - 'formcandiconfig-showfieldsort' => json_encode($fields), - 'formcandiconfig-othercolnamea' => 'P16LabelZh
R&D', - 'formcandiconfig-othercolnameae' => 'P16LabelEn
R&D', - ]); $candidateOriginal = $readFields($manualUrl, ['formcandidata-othercola', 'formcandidata-othercolae', 'formcandidata-othercolb', 'formcandidata-othercolbe']); $saveFields($manualUrl, [ 'formcandidata-othercola' => 'P16Zh
R&D', @@ -78,6 +84,9 @@ foreach (['zh-TW' => 'Zh', 'en-US' => 'En'] as $language => $locale) { foreach (['ballot-work/result', 'result/index', 'vote/result', 'vote/vote-detail', 'ballot/edit'] as $page) { $extra = $page === 'ballot/edit' ? '&party=0&ballotID=1' : ''; + if ($page === 'vote/vote-detail') { + $extra .= '&questionID='.$questionId; + } $status = $admin->go('/'.$page.'?voteID='.$vote.$extra.'&hl='.$language); $r = $admin->js('(() => { const a = [...document.querySelectorAll("td")].find(e => e.textContent.includes("P16'.$locale.'")); @@ -124,5 +133,7 @@ $saveFields($manualUrl, $candidateOriginal); } $saveFields($resultUrl, $originalResults); - $saveFields($configUrl, $originalConfig); + foreach ($originalConfigs as $url => $originalConfig) { + $saveFields($url, $originalConfig); + } } From 35ba43cdaadf0e5c8396c0750e1fb8dfd75469af Mon Sep 17 00:00:00 2001 From: TUNG-YEN Date: Fri, 6 Nov 2026 11:11:53 +0800 Subject: [PATCH 7/8] Render custom field A with safe markup in candidate management The admin candidate list still showed field A line breaks as literal tags, unlike the voting, result and ballot screens. Apply the same purified output there; fields B-F stay plain text. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 +- tests/functional/ResultFieldRenderingCest.php | 44 +++++++++++++++++++ views/candi/data.php | 7 +++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6659d09..10a1b8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,7 +40,7 @@ ### Fixed -* 開票結果、投票結果、候選名單與選票編輯表格消除重複 HTML 編碼;自訂標題及欄 A 保留經淨化的換行與排版,欄 B–F 維持純文字,既有 entity 不作資料轉換 +* 開票結果、投票結果、候選名單與選票編輯表格消除重複 HTML 編碼;自訂標題及欄 A 保留經淨化的換行與排版(管理端候選人管理列表的欄 A 一併適用),欄 B–F 維持純文字,既有 entity 不作資料轉換 * 英文介面的紙本選票自訂文字欄位誤讀多一個 E 的欄名而發生錯誤;保留中英並列與原有數字欄位的語言選擇 * Yii log 檢視頁的 UrlModal 資產指向未隨 Git 提供的目錄而發生 500;改為追蹤前端腳本,內容載入沿用目前頁面的協定與主機 * 殘留的 Bootstrap 4 樣式在 Bootstrap 5 下失效:輪次與投票狀態徽章看不見、對話框關閉鈕錯位,核取方塊、輸入群組、間距與邊框也一併改為 Bootstrap 5 寫法 diff --git a/tests/functional/ResultFieldRenderingCest.php b/tests/functional/ResultFieldRenderingCest.php index 5b5d55f..d958f67 100644 --- a/tests/functional/ResultFieldRenderingCest.php +++ b/tests/functional/ResultFieldRenderingCest.php @@ -167,4 +167,48 @@ public function testRendering(FunctionalTester $I, Example $case): void } } } + + public function testCandidateManagementRendersFieldAOnce(FunctionalTester $I): void + { + $voteID = 'AnonPartyTest'; + $transaction = Yii::$app->db->beginTransaction(); + try { + $candidate = CandiData::find()->where(['voteID' => $voteID])->orderBy('id')->one(); + Assert::assertNotNull($candidate); + $config = CandiConfig::findOne(['voteID' => $voteID]); + Assert::assertNotNull($config); + $config->updateAttributes(['showFieldSort' => json_encode(array_map( + fn ($id) => ['id' => $id, 'children' => []], + ['Name', 'otherColA', 'otherColB'] + ))]); + $candidate->updateAttributes([ + 'otherColA' => 'MA
R&D', + 'otherColB' => 'MB
R&D', + ]); + $I->amOnPage('/candi/data?voteID='.$voteID); + $I->seeResponseCodeIs(200); + $dom = new DOMDocument(); + $previous = libxml_use_internal_errors(true); + try { + $dom->loadHTML(''.$I->grabPageSource()); + } finally { + libxml_clear_errors(); + libxml_use_internal_errors($previous); + } + $xpath = new DOMXPath($dom); + Assert::assertSame(0, $xpath->query('//*[@onclick="p16attack()"]')->length, 'Event handlers are removed'); + $cells = $xpath->query('//td[contains(., "MA")]'); + Assert::assertGreaterThan(0, $cells->length, 'Field A is present'); + Assert::assertGreaterThan(0, $xpath->query('.//br', $cells->item(0))->length, 'Field A has a real break'); + Assert::assertSame('R&D', $xpath->query('.//b', $cells->item(0))->item(0)->textContent); + $plain = $xpath->query('//td[contains(., "MB")]'); + Assert::assertGreaterThan(0, $plain->length, 'Field B is present'); + Assert::assertSame(0, $xpath->query('.//br', $plain->item(0))->length, 'Field B stays plain text'); + Assert::assertSame('MB
R&D', trim($plain->item(0)->textContent)); + } finally { + if ($transaction->isActive) { + $transaction->rollBack(); + } + } + } } diff --git a/views/candi/data.php b/views/candi/data.php index 7c31297..a55af49 100644 --- a/views/candi/data.php +++ b/views/candi/data.php @@ -1,5 +1,6 @@ [ 'header' => Model::i18n($candiConfig->otherColNameAE, $candiConfig->otherColNameA, false), 'attribute' => 'otherColA', + // 自定義欄位 1 允許經淨化的排版(與投票頁、結果頁一致);欄位 2–6 維持純文字 + 'format' => 'raw', + 'value' => function ($model, $key, $index, $column) + { + return HtmlPurifier::process($model->otherColA, Yii::$app->params['HtmlPurifier.config']); + }, 'headerOptions' => ['class' => 'align-middle text-center'], 'contentOptions' => ['class' => 'align-middle text-center'], ], From 083f26c03d2a49e99e927c8e5e71bf564f7e04f5 Mon Sep 17 00:00:00 2001 From: TUNG-YEN Date: Fri, 6 Nov 2026 11:18:25 +0800 Subject: [PATCH 8/8] Apply candidate list test fields to every question configuration Co-Authored-By: Claude Opus 5.5 --- tests/functional/ResultFieldRenderingCest.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/functional/ResultFieldRenderingCest.php b/tests/functional/ResultFieldRenderingCest.php index d958f67..518e837 100644 --- a/tests/functional/ResultFieldRenderingCest.php +++ b/tests/functional/ResultFieldRenderingCest.php @@ -175,12 +175,11 @@ public function testCandidateManagementRendersFieldAOnce(FunctionalTester $I): v try { $candidate = CandiData::find()->where(['voteID' => $voteID])->orderBy('id')->one(); Assert::assertNotNull($candidate); - $config = CandiConfig::findOne(['voteID' => $voteID]); - Assert::assertNotNull($config); - $config->updateAttributes(['showFieldSort' => json_encode(array_map( + // The page may use any question's configuration, so show the fields in all of them. + Assert::assertGreaterThan(0, CandiConfig::updateAll(['showFieldSort' => json_encode(array_map( fn ($id) => ['id' => $id, 'children' => []], ['Name', 'otherColA', 'otherColB'] - ))]); + ))], ['voteID' => $voteID])); $candidate->updateAttributes([ 'otherColA' => 'MA
R&D', 'otherColB' => 'MB
R&D',